#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # Copyright by Scott Severance # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. ################################################################################ # Invoke this program with --help for an abbreviated help message. # # # # Compatibility note: This program has only been tested with Python 2. It will # # not work without modification in Python 3. # ################################################################################ ''' Tool for synchronizing fonts installed on different Ubuntu machines Consider the following scenario: You have multiple Ubuntu machines (other OSes that use the APT package management system might work as well, but they haven't been tested). You need to have a common set of fonts on all the machines and you want each machine to have every font installed on at least one other machine. Furthermore, each machine has write access to a common shared directory. If the above scenario describes your situation, then this tool can help. Using it is a two-step process: 1. Invoke this tool on each machine, passing the --write option and a target directory (which will be created if it doesn't exist). The best target is one that all the machines concerned can write to. This will produce a file in the target directory named after the machine's hostname which contains a list of all[1] the font packages installed on the system. [1]: This tool doesn't recognize absolutely all font packages. Some tex/latex and Type 1 fonts aren't included. This is because the package names are inconsistent and I don't care about those fonts anyway. If you care about them, you'll have to manually add them to installed_fonts(). 2. Invoke this tool a second time on each machine, passing the --read option. It will print a list of font packages that need to be installed in order to bring the machine up to date. The easiest way to do this is to make the output the input to apt-get. See the example below. EXAMPLE ------- This example assumes the following: 1. Each machine is accessible via SSH 2. font_lister.py is already available on each machine's $PATH 3. The common shared directory is available to each machine on the path /media/shared/font_lister 4. The machine hostnames are foo, bar, and baz You might throw together a bash script similar to the following: #!/bin/bash machines="foo bar baz" for i in $machines; do ssh "$i" font_lister.py -w /media/shared/font_lister done for i in $machines; do ssh "$i" 'bash -c "sudo apt-get install $(font_lister.py --read /media/shared/font_lister)"' done ''' import apt import argparse import os import re import socket import sys def installed_fonts(): ''' Returns a list of the font packages installed on the system. Some tex/latex fonts are missing from this list. The package names are inconsistant and thus difficult to list, and I don't care about tex/latex anyway. Also, some Type 1 fonts are missing for the same reason. ''' regex1 = re.compile(r'^([x]?fonts|ttf|language-support-fonts|cm-super|ko.tex|ming-fonts|texlive-fonts)-') regex2 = re.compile(r'-fonts$') others = ('culmus','culmus-fancy','gsfonts-x11','lmodern','cm-super', 'fonty-rg','gsfonts-other','jsmath-fonts-sprite','konfont', 'msttcorefonts','otf-freefont','tex-gyre','ubuntu-studio-font-meta', 'unifont','vfdata-morisawa5') return [i.name for i in apt.Cache() if i.is_installed and ( i.name in others or regex1.search(i.name) or regex2.search(i.name))] def write_file(target_dir, data): ''' Writes data to a file in target_dir. The file is called hostname.fonts.list, where hostname is the machine's hostname as reported by socket.gethostname(). ''' output_name = os.path.join(target_dir, socket.gethostname() + '.fonts.list') try: with open(output_name, 'w') as f: f.write(data) except IOError: error_exit('Failed to write the file "%s".' % output_name) def error_exit(msg, status=1): ''' Prints an error to stderr including msg, then exits with the given status. ''' sys.stderr.write('ERROR: ' + msg + '\n') sys.exit(status) def write_installed_fonts(target_dir, fonts): ''' Prepares to write the fonts, one per line, to target_dir, creating it if necessary. ''' target_dir = os.path.abspath(target_dir) data = '\n'.join(fonts) if os.path.exists(target_dir): if os.path.isdir(target_dir): write_file(target_dir, data) else: error_exit('Target destination is not a directory.') else: try: os.makedirs(target_dir) except OSError: error_exit('Could not create the directory "%s".' % target_dir) write_file(target_dir, data) def list_fonts_to_install(target_dir): ''' Returns a list of fonts to be installed to bring this machine into sync. This function reads all the ".fonts.list" files in target_dir, merges them into a single list, weeds out duplicates, and removes those fonts that are already installed on this system. It then returns a list containing the remaining fonts, which represents the fonts that need to be installed in order to synchronize this machine. ''' target_dir = os.path.abspath(target_dir) name_re = re.compile(r'\.fonts\.list$') try: machines = [open(os.path.join(target_dir, i)).read().split('\n') for i in os.listdir(target_dir) if name_re.search(i)] except IOError: error_exit("Couldn't read one or more files in \"%s\"." % target_dir) except OSError: error_exit('The directory "%s" doesn\'t exist or is otherwise inaccessible.' % target_dir) all_fonts = [] # flatten the multiple lists in machines into a single list: [[all_fonts.append(i) for i in j] for j in machines] all_fonts = sorted(list(set(all_fonts))) # remove duplicates local_fonts = installed_fonts() return [i for i in all_fonts if i not in local_fonts] def main(): ''' Processes the command line arguments and takes action as directed by those options. ''' parser = argparse.ArgumentParser( description='A tool for synchronizing the fonts installed on ' + 'different Ubuntu machines. You MUST specify either --write or ' + '--read.') group = parser.add_mutually_exclusive_group(required=True) group.add_argument('-w', '--write', metavar='target_dir', help='Write the installed fonts to a file in target_dir, ' + 'which will be created if necessary. This directory ' + 'shouldn\'t be used by any other program.') group.add_argument('-r', '--read', metavar='target_dir', help='Read all machines\' installed fonts and prepare a ' + 'list of fonts to install on this machine to bring it in ' + 'sync. Specify target_dir, which is the directory where ' + 'the data files live.\nWARNING: The data files aren\'t ' + 'checked to ensure that they don\'t contain malicious ' + 'or invalid contents. You have been warned.') args = parser.parse_args() if args.write: # we were called with --write write_installed_fonts(args.write, installed_fonts()) else: # we were called with --read print '\n'.join(list_fonts_to_install(args.read)) if __name__ == '__main__': main()