#!/usr/bin/env python """ This program was written to automatically check for, download, and/or apply bug fixes for Amber. There are 4 major modes or functionalities that this script contains. --check-updates : checks for updates on the Amber bug fix server --patch-level : returns which bug fixes have been applied --download-patches : downloads the patches that have not been applied --update : downloads the patches that have not been applied and patch Amber with those patches This program is initially written with a single, ASCII patch process in mind, but is designed to be easily extended to include different bugfix protocols as they become necessary (it supports bzip2- and gzip-compressed bug fixes as well and will automatically decompress them). This script was conceptualized and written by Jason Swails from the research group of Adrian Roitberg, and is released pursuant to the Amber and/or AmberTools licenses, whichever is applicable (do not remove this notice). Acknowledgements/Contributions from others * Ben Roberts made helpful contributions to the user-interface * Tyler Luchko made helpful contributions to the user-interface """ # Load common os module and package into top level namespace from os import path, getenv # Global variables. Other ones that change less frequently can be found below # the Patch class definitions. # # __version__ : Version of __this script__ # # BUGFIX_PREFIX : The prefix used on the Amber website for all bug fixes # # patch_locs : Folders online where patches for this package are, NOT # where the links to download them are, but where the # actual files themselves are. Supports arbitrary number # of repositories, but each separate repository of bug # fixes MUST have an associated unapplied_patch_loc, # applied_patch_loc, and patch_desc_list entry! # # unapplied_patch_loc : local folder where downloaded patches are put. The # .patches directory is hard-coded in several places in # this script, so take care if you decide to change this # # applied_patch_loc : local folder where applied patches are put. All # patches applied from an unapplied_patch_loc is put in # its corresponding applied_patch_loc, which is default # location for --update-tree, but not for --apply-patch # necessarily # # patch_desc_list : Patch Description List -- it's a short description for # each repository in patch_locs # # check_repo : stores whether we should check the repository for # patches or not (i.e. DO NOT check for Amber 12 patches # if all we have is AmberTools 12) Determine if we have # Amber 12 by checking the existence of src/Makefile __version__ = "12.0" BUGFIX_PREFIX = 'bugfix' patch_locs = ['file:///usr/local/bugfixes/AmberTools/12.0/', 'file:///usr/local/bugfixes/12.0/'] unapplied_patch_loc = [path.join('.patches', 'AmberTools_Unapplied_Patches'), path.join('.patches', 'Amber_Unapplied_Patches')] applied_patch_loc = [path.join('.patches', 'AmberTools_Applied_Patches'), path.join('.patches', 'Amber_Applied_Patches')] patch_desc_list = ['AmberTools12', 'Amber12'] check_repo = [True, path.exists(path.join('src', 'Makefile'))] #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ import os import sys, urllib, re, signal, socket from optparse import OptionParser, OptionGroup from subprocess import Popen, PIPE # urlopen is deprecated in Python2.6 and removed in Python3. Therefore, use # urllib2's urlopen if we can't use the one from urllib. This is really just # used to make sure that the file exists. try: from urllib import urlopen, addinfourl except ImportError: from urllib2 import urlopen #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ # Set up a signal handler. We don't want to return a positive number, since that # indicates we have that many unapplied patches. Therefore, return with -1 (so # we know there was an error). def interrupt_handler(signal, frame): print >> sys.stderr, 'Caught SIGINT. Exiting cleanly.' sys.exit(1) signal.signal(signal.SIGINT, interrupt_handler) #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ class IllegalFileError(Exception): """ Raise this if we try to modify an illegal (i.e. not Amber) file """ #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ # urlopen.getcode() was implemented in Python 2.6, so for earlier Python's we # have to fake it. We do this by looking for Last-Modified: in the info() string # If it's present, then it's been edited before. If it's not, the file doesn't # exist (note this works for the bug fixes because they're plain files, so it # should be a robust solution for this application, even if it is a hack). if not hasattr(addinfourl, 'getcode'): def getcode(self): if 'Last-Modified:' in str(self.info()): return 200 else: return 404 addinfourl.getcode = getcode #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ def _which(program): """ Looks for a program in the PATH """ def is_exe(filename): return os.path.exists(filename) and os.access(filename, os.X_OK) # Check to see that a path was provided in the program name fpath, fname = path.split(program) if fpath and is_exe(program): return program # Look through the path and see if we can find our EXE program anywhere there for mypath in getenv('PATH').split(os.pathsep): exe_file = path.join(mypath, program) if is_exe(exe_file): return exe_file return None # if program can still not be found... return None #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ class Patch(object): """ Basic Patch class. This is specifically for ASCII-type patches, but should be subclassed for different types of patches. """ #======================================== def __init__(self, patch_file): """ Saves the filename and loads the header information """ self.name = patch_file # Find out which number patch we are so we can sort the patches try: self.number = int(path.split(self.name)[1].strip(BUGFIX_PREFIX+'.')) except ValueError: self.number = 0 if not path.exists(patch_file): sys.stderr.write('Fatal Error: Could not open local patch file ' + self.name + '\n') sys.exit(1) # Compile the regexp to indicate the last line of the header. It's # indicated by a line of -------------------'s end_line = re.compile(r'-+') header_ended = False # Load the header self.header = '' tmpfile = open(patch_file, 'r') for line in tmpfile: # Make sure the end_line matches and it's just a line of -'s if end_line.match(line.strip()) and not end_line.sub('', line.strip()): tmpfile.close() header_ended = True break self.header += line if not header_ended: sys.stderr.write('Fatal Error: Badly formatted patch file %s. ' % ( self.name) + 'Could not find end of header.\n') sys.exit(1) #======================================== def description(self): """ Glean the description out of the header """ # Allow us to not have a header (for maybe-future non-ASCII patch files) if not hasattr(self, 'header') or not self.header: return 'No header information in patch %s' % self.name if not 'Description' in self.header: return 'Could not find a valid description in %s' % self.name return self.header[self.header.index('Description'):] #======================================== def author(self): """ Get the author from the header """ # Allow us to not have a header (for maybe-future non-ASCII patch files) return self._get_attr(re.compile(r' *[Aa]uthor\(*s*\)*:*')) #======================================== def date(self): """ Get the date patch was made from the header """ return self._get_attr(re.compile(r' *[Dd]ate:* *')) #======================================== def programs(self): """ Get the program edited from the header """ progs = self._get_attr(re.compile(r' *[Pp]rogram\(*s*\)*:*')) if progs: ret = progs.strip().replace(',',' ').split() else: ret = None return ret #======================================== def _get_attr(self, regexp): """ Returns an attribute from the header """ if not hasattr(self, 'header') or not self.header: return None for line in self.header.split('\n'): if regexp.match(line): return regexp.sub('', line).strip() return None #======================================== def files_edited(self): """ Parse through the patch file and find out which files are being modified """ # This regex matches lines that start like "+++ path/to/file.cpp" modfile = re.compile(r'\+\+\+ \.*\/*[\w\-\/\.\+]+[\w\-\.\+]+\.*\w*') selffile = re.compile('\\+\\+\\+ %s' % path.split(sys.argv[0])[1]) patch = open(self.name, 'r') files_modified = [] for line in patch: if modfile.match(line): filename = modfile.findall(line)[0].strip('+++').strip() # Make sure our modifications stay inside AMBERHOME for security if filename.startswith('../'): raise IllegalFileError("Detected patched file outside " + "AMBERHOME [%s] -- For security this is not allowed" % filename) elif filename.startswith('/') and filename != '/dev/null': raise IllegalFileError("Detected patched file with absolute " + "path: [%s] -- For security this is not allowed." % filename) if not filename in files_modified: files_modified.append(filename) elif selffile.match(line): filename = path.split(sys.argv[0])[1] if not filename in files_modified: files_modified.append(filename) patch.close() return files_modified #======================================== def apply_patch(self, undo=False): """ Applies the patch file using "patch -p0 -N" and moves the file to the new directory given in new_dest """ patch_file = open(self.name, 'r') patch = _which('patch') if not patch: sys.stderr.write('Fatal Error: Cannot find the patch program.\n') sys.exit(1) # Allow us to undo patches if undo: args = [patch, '-p0', '-N', '-R', '-s'] else: args = [patch, '-p0', '-N', '-s'] process = Popen(args, stdin=patch_file, stdout=PIPE, stderr=PIPE) (output, error) = process.communicate('') ret_code = process.wait() if ret_code: sys.stderr.write('Error or warning during patching process for %s:\n' % (self.name)) sys.stderr.write(output + '\n' + error + '\n') return ret_code #======================================== def move(self, new_dir, silent=False): """ Moves this patch to the new path """ fname = path.split(self.name)[1] os.rename(self.name, path.join(new_dir, fname)) if not silent: print ' Moving %s to %s' % (fname, new_dir.replace('.patches/','',1)) # Update our new name self.name = path.join(new_dir, fname) #======================================== def delete(self): """ Removes the patch file """ os.remove(self.name) #======================================== # Rank patches by the number from bugfix.number def __gt__(self, other): return self.number > other.number def __lt__(self, other): return self.number < other.number def __eq__(self, other): return self.number == other.number def __le__(self, other): return self.number <= other.number def __ge__(self, other): return self.number >= other.number def __ne__(self, other): return self.number != other.number #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ class PatchBz2(Patch): """ A bzip2-ed patch """ #======================================== def __init__(self, patch_file): """ First, bunzip2 this file then load the Patch constructor with the now-ASCII patch file :) """ if not patch_file.endswith('.bz2'): raise TypeError('Expecting bzip2-compressed patch file, but got ' + 'file named [ %s ] instead' % patch_file) if not path.exists(patch_file): raise IOError('Cannot find patch file [ %s ]' % patch_file) bunzip = _which('bunzip2') if not bunzip: sys.stderr.write('Cannot process bz2-compressed patches without ' + 'bunzip2\n') sys.exit(1) print 'Decompressing %s with %s' % (patch_file, bunzip) process = Popen([bunzip, patch_file], stdout=PIPE, stderr=PIPE) (output, error) = process.communicate('') if process.wait(): sys.stderr.write('Fatal Error: Could not decompress %s\n%s\n' % ( patch_file, error)) sys.exit(1) # Get rid of the file extension patch_file = patch_file.strip('.bz2') Patch.__init__(self, patch_file) #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ class PatchGz(Patch): """ A gzip-ed patch """ #======================================== def __init__(self, patch_file): """ First, gunzip this file then load the Patch constructor with the now-ASCII patch file :) """ if not patch_file.endswith('.gz'): raise TypeError('Expecting gzip-compressed patch file, but got ' + 'file named [ %s ] instead' % patch_file) if not path.exists(patch_file): raise IOError('Cannot find patch file [ %s ]' % patch_file) gunzip = _which('gunzip') if not gunzip: sys.stderr.write('Cannot process gz-compressed patches without ' + 'gunzip\n') sys.exit(1) print 'Decompressing %s with %s' % (patch_file, gunzip) process = Popen([gunzip, patch_file], stdout=PIPE, stderr=PIPE) (output, error) = process.communicate('') if process.wait(): sys.stderr.write('Fatal Error: Could not decompress %s\n%s\n' % ( patch_file, error)) sys.exit(1) # Get rid of the file extension patch_file = patch_file.strip('.gz') Patch.__init__(self, patch_file) #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ # The list of searched suffixes for patch names (ASCII, Bzip2, Gzip) patch_suffixes = ('', '.bz2', '.gz') # The corresponding classes for each patch suffix listed above IN SAME ORDER patch_classes = (Patch, PatchBz2, PatchGz) # This is the description printed with the program usage message epilog = """This program automates the checking, downloading, and applying of bug fixes for Amber and AmberTools""" #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ def download_patch(search_location, patch_number, save_loc): """ This looks in the search_location URL for bugfix., along with the .bz2 and .gz suffixes to allow for compressed patches, and stores them to a given storage location. The searched URL is """ global BUGFIX_PREFIX, patch_suffixes, patch_classes # First search for every allowed version of the bugfix for i, suffix in enumerate(patch_suffixes): PatchClass = patch_classes[i] fixname = '%s.%d%s' % (BUGFIX_PREFIX, patch_number, suffix) # Catch timeout errors, but re-raise the IOError if it's not a timeout try: url_patch = urlopen(search_location + fixname) except IOError, err: if 'timed out' in str(err): print >> sys.stderr, 'Timed out connecting to server' sys.exit(1) raise err # A 404 code means we didn't find it if url_patch.getcode() == 404: url_patch.close() continue # Otherwise we found it, so use urlretrieve to download the file url_patch.close() local_patch = path.join(save_loc, fixname) print ' Downloading %s' % fixname urllib.urlretrieve(search_location + fixname, local_patch) # Now that we have the patch file saved, return the appropriate PatchClass return PatchClass(local_patch) # We couldn't find anything at this point return None #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ def patch_description(search_location, patch_number, verbose): " Gets a description of a patch without downloading it. Only work on ASCII " global BUGFIX_PREFIX, patch_suffixes, patch_classes # First search for every allowed version of the bugfix for i, suffix in enumerate(patch_suffixes): fixname = '%s.%d%s' % (BUGFIX_PREFIX, patch_number, suffix) try: url_patch = urlopen(search_location + fixname) except IOError, err: if 'timed out' in str(err): print >> sys.stderr, 'Timed out connecting to server' sys.exit(1) else: continue # A 404 code means we didn't find it if url_patch.getcode() == 404: url_patch.close() else: # If this is an ASCII patch, get the description from the header and # return that. Otherwise return "not an ascii file" if "Content-Type: text/plain" in str(url_patch.info()): # End Of Description regular expression eod_re = re.compile('-+') text = '' for line in url_patch: if eod_re.match(line.strip()) and not eod_re.sub('', line.strip()): break if not verbose and line.strip().startswith('Description'): break text += line url_patch.close() return text else: url_patch.close() return 'Non-ASCII patch file' return '' #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ def describe_patch(patch, full=True, hide_prefix=True): """ This formats the description of a given patch If full is True, then we include the full description. Otherwise, we just print the Author, date, and programs fixed """ if hide_prefix: print 'Patch %s:' % patch.name.strip('.patches/') else: print 'Patch %s:' % patch.name if patch.author(): print ' Author: ', patch.author() if patch.date(): print ' Created on: ', patch.date() if patch.programs(): print ' Programs fixed: ', ', '.join(patch.programs()) if patch.files_edited(): print ' Files affected: ', ('\n%19s' % ' ').join(patch.files_edited()) if full: print '\n', patch.description() print '' #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ def _mkdir(pathname): """ Makes a directory recursively if it's not already a directory """ if not path.isdir(pathname): os.makedirs(pathname) #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ def _files_present(patch): """ This looks through all of the files edited via the given patch and returns a code based on what it finds out. return 0 : This patch tries to edit files just in sander and/or pmemd, and we don't have that source code installed here return 1 : We're good to go. Any files that are new, just create here (as long as they are not in src/sander or src/pmemd) return -1: BAD -- some of the files are in src/sander and some in src/pmemd, but not all of them are. Please avoid this, developers. return 2 : This program (AMBERHOME/sys.argv[0]) is modified, but everything else is OK. return -2: Same as -1, except we also modify this program (as in 2) """ edfiles = patch.files_edited() if not edfiles: print >> sys.stderr, ('Fatal Error: The patch "%s" does not edit any ' + 'files!') % patch.name sys.exit(1) fname = edfiles[0] ret_code = 1 if ('src/sander' in fname or 'src/pmemd' in fname) and not path.isdir( path.join(getenv('AMBERHOME'), 'src', 'pmemd', 'src')): ret_code = 0 if fname == path.split(sys.argv[0])[1]: ret_code = 2 for i in range(1,len(edfiles)): fname = edfiles[i] # If ret_code is already 1, then our first file matched one we had, # but now this one doesn't. That's bad news if ('src/sander' in fname or 'src/pmemd' in fname) and not path.isdir( path.join(getenv('AMBERHOME'), 'src', 'pmemd', 'src')): if ret_code == 1: ret_code = -1 if fname == path.split(sys.argv[0])[1]: if ret_code in [-1, 0]: ret_code = -2 else: ret_code = 2 # This file doesn't exist, so just create it. But create the containing # directories first if they don't already exist if not path.exists(path.join(getenv('AMBERHOME'), fname)): _mkdir(path.join(getenv('AMBERHOME'), path.split(fname)[0])) print 'Creating file %s' % (path.join(getenv('AMBERHOME'), fname)) tmpfile = open(path.join(getenv('AMBERHOME'), fname), 'w') tmpfile.close() return ret_code #~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~+~ if __name__ == '__main__': """ Main part of this script """ # Make sure we have AMBERHOME set and that it exists if not getenv('AMBERHOME'): print >> sys.stderr, 'Fatal Error: AMBERHOME is not set!' sys.exit(1) # We need to make sure that patch_amber.py resides in AMBERHOME. So we will # get the path of the script from sys.argv[0] and expand it using abspath() # (and resolve any symlinks using realpath() on it). We can then compare this # to AMBERHOME to make sure that AMBERHOME is set to the correct directory. # This way, patch_amber.py can be executed from anywhere and we can be sure # AMBERHOME is set correctly (unless people move this script--DON'T DO THAT) path_of_script = path.realpath(path.abspath(path.split(sys.argv[0])[0])) if path.realpath(getenv('AMBERHOME')) != path_of_script: print >> sys.stderr, ('Fatal Error: AMBERHOME is set to [%s] but I ' + 'think it should be [%s]. ') % (getenv('AMBERHOME'), path_of_script) print >> sys.stderr, (' Please set AMBERHOME to the ' + 'proper directory') sys.exit(1) # Now that we have performed our setup, make the option parser parser = OptionParser(version=__version__) group = OptionGroup(parser, 'Normal Operating Modes', 'These are the ' + 'different operating modes for this script. Only one ' + 'of these may be specified at a time.') group.add_option('--patch-level', dest='patch_level', default=False, action='store_true', help='Print a report on which ' + 'bug fixes have already been applied') group.add_option('--check-updates', dest='check_updates', default=False, action='store_true', help='Check for bugfixes that ' + 'have not been applied on ambermd.org') group.add_option('--update-tree', dest='update_tree', default=False, action='store_true', help='Download any unapplied bug ' + 'fixes from ambermd.org and apply them') parser.add_option_group(group) parser.add_option('--timeout', dest='timeout', type='float', default=10.0, help='How long (sec.) to wait for connections to the ' + 'Amber website. Default %default') parser.add_option('--ignore-fails', dest='ignore_fails', default=False, action='store_true', help='If a particular patch fails ' + 'applying a HUNK, accept it and soldier on. CAUTION ' + 'using this option!') group = OptionGroup(parser, 'Advanced Operating Modes', 'The following options are '+ 'also Operating Modes (so they cannot be specified ' + 'with any of the options above). They provide the ' + 'ability to work with a single patch at a time. If you' + ' are not confident with what you are doing, this ' + 'could mess up your tree. If you modify the directory ' + 'containing the downloaded and managed patches, you ' + 'will most likely break this script. Use caution!') group.add_option('--download-patches', dest='download_patches', default=False, action='store_true', help='Download any ' + 'unapplied patches and put them in the unapplied patches' + ' directories of $AMBERHOME/.patches but do NOT apply them') group.add_option('--apply-patch', dest='apply_patch', default=None, help='Apply only a specific patch. If it is in one of the '+ 'Unapplied patch folders, it is moved to the Applied ' + 'patch folder.', metavar='PATCH_FILE') group.add_option('--reverse-patch', dest='reverse_patch', default=None, help='Patch file to apply in reverse. If it is in one of ' + 'the Applied patch folders, it is moved to the Unapplied ' + 'patch folder. Note if not done carefully, this could fail', metavar='PATCH_FILE') group.add_option('--show-applied-patches', dest='show_applied', default=False, action='store_true', help='Show names and ' + 'location of each applied patch, along with details ' + 'according to the verbosity flags used.') group.add_option('--show-unapplied-patches', dest='show_unapplied', default=False, action='store_true', help='Show names and ' + 'location of each unapplied patch, along with details ' + 'according to the verbosity flags used.') parser.add_option_group(group) group = OptionGroup(parser, 'Verbosity Flags', 'These options control ' + 'how much information is printed about each patch') group.add_option('--verbose', dest='full_desc', action='store_true', default=False, help='Print patch statistics and detailed descriptions.') group.add_option('--quiet', dest='silent', default=False, action='store_false', help='Do not print detailed patch ' + 'descriptions -- just patch statistics.') group.add_option('--dead-silent', dest='silent', action='store_true', default=True, help='Minimal information. You get no details about the ' + 'individual patches. This is the default.') parser.add_option_group(group) # Now parse our command-line arguments (opt, args) = parser.parse_args() if args: print >> sys.stderr, 'Bad command-line arguments!' parser.print_help() sys.exit(1) if opt.full_desc: opt.silent = False # Now make sure we were asked to do _something_, but we didn't try to do 2 # incompatible things. Since every argument is mutually exclusive, we can # just add the bools, which is transformed to an int (True = 1, False = 0). sum_options = (opt.patch_level + opt.check_updates + opt.update_tree + opt.download_patches + opt.show_applied + opt.show_unapplied + bool(opt.apply_patch) + bool(opt.reverse_patch)) if sum_options == 0: parser.print_usage() print """Options: --update-tree Download and apply all patches now --patch-level See which patches have been applied --check-updates See if any unapplied updates are available --timeout How long to wait to see if http://ambermd.org is up --help/-h More detailed options """ sys.exit(0) if sum_options > 1: print >> sys.stderr, 'Fatal Error: All operating modes are mutually ' + \ 'exclusive!' parser.print_help() sys.exit(1) # Go to AMBERHOME, this is where we have to apply patches from os.chdir(getenv('AMBERHOME')) # Make the patch directories. _mkdir does nothing if this directory already # exists try: for i in range(len(patch_locs)): _mkdir(unapplied_patch_loc[i]) _mkdir(applied_patch_loc[i]) except OSError, err: print >> sys.stderr, 'Error creating patch directories\n', err sys.exit(1) # Find which patches we've already applied. applied_patches = [[] for i in range(len(applied_patch_loc))] for i in range(len(applied_patches)): if not check_repo[i]: continue # skip any repos we shouldn't check abspath = applied_patch_loc[i] applied_patches[i] = [Patch(path.join(abspath, fname)) for fname in os.listdir(abspath)] applied_patches[i].sort() # Now determine the last patch number that was applied in each case last_patch_applied = [0 for i in range(len(applied_patches))] for i in range(len(applied_patches)): if not check_repo[i]: continue # skip any repos we shouldn't check for ipatch in applied_patches[i]: # Assume it's BUGFIX_PREFIX.#, and the # is what we want try: fname = path.split(ipatch.name)[1] test = int(fname.replace(BUGFIX_PREFIX + '.', '').strip()) last_patch_applied[i] = max(last_patch_applied[i], test) except ValueError: print >> sys.stderr, 'Warning: Unknown number of bugfix %s' % \ ipatch.name #~~~~~~~~~~~~~~~~~~~~ Code block for --patch-level ~~~~~~~~~~~~~~~~~~~~~~~~~ # If all we wanted was the patch level, print that out here if opt.patch_level: for i, desc in enumerate(patch_desc_list): if not check_repo[i]: continue # skip any repos we shouldn't check if last_patch_applied[i] == 0: print 'No patches have been applied to %s yet.' % desc continue size = max([len(l)+1 for l in patch_desc_list]) text = 'Latest patch applied to %%-%ds %%d' % size print text % (desc+':', last_patch_applied[i]) sys.exit(0) #~~~~~~~~~~~~~~~~~ Code block for --show-applied-patches ~~~~~~~~~~~~~~~~~~~ # Now if we wanted to just show the applied patches if opt.show_applied: for i, desc in enumerate(patch_desc_list): if not check_repo[i]: continue # skip any repos we shouldn't check print 'Looking for applied %s patches:' % desc for ipatch in applied_patches[i]: if not opt.silent: describe_patch(ipatch, full=opt.full_desc, hide_prefix=False) else: print ' %s' % ipatch.name if applied_patches[i]: print '' sys.exit(0) #~~~~~~~~~~~~~~ Code block for --show-unapplied-patches ~~~~~~~~~~~~~~~~~~~ # Now if we wanted to just show the unapplied patches if opt.show_unapplied: # Find which patches we've downloaded but not applied unapplied_downloads = [[] for i in range(len(unapplied_patch_loc))] for i in range(len(unapplied_downloads)): if not check_repo[i]: continue # skip any repos we shouldn't check abspath = unapplied_patch_loc[i] unapplied_downloads[i] = [Patch(path.join(abspath, fname)) for fname in os.listdir(abspath)] unapplied_downloads[i].sort() for i, desc in enumerate(patch_desc_list): if not check_repo[i]: continue # skip any repos we shouldn't check print 'Looking for unapplied %s patches:' % desc for ipatch in unapplied_downloads[i]: if not opt.silent: describe_patch(ipatch, full=opt.full_desc, hide_prefix=False) else: print ' %s' % ipatch.name if unapplied_downloads[i]: print '' sys.exit(0) #~~~~~~~~~~~~~ Code block for --apply-patch AND --reverse-patch ~~~~~~~~~~~~ # Apply or reverse a local patch, and it MUST be ascii if opt.apply_patch or opt.reverse_patch: if (opt.apply_patch and not path.exists(opt.apply_patch)) or ( opt.reverse_patch and not path.exists(opt.reverse_patch)): print >> sys.stderr, 'Fatal Error: Cannot find %s to apply it!' % ( opt.apply_patch) sys.exit(1) if opt.apply_patch: single_patch = Patch(opt.apply_patch) else: single_patch = Patch(opt.reverse_patch) if not single_patch.files_edited(): print >> sys.stderr, 'Fatal Error: Bad patch. Could not find the ' + \ 'files that were supposed to be edited!' # Print out message based on whether we're applying forward or reverse if opt.apply_patch: print 'Applying patch %s:' % opt.apply_patch else: print 'Reversing patch %s:' % opt.reverse_patch # Describe the patch if so desired if not opt.silent: describe_patch(single_patch, full=opt.full_desc) # Apply the patch, and analyze its return code. If non-zero, it failed. if single_patch.apply_patch(undo=bool(opt.reverse_patch)): if not opt.ignore_fails: print >> sys.stderr, 'Quitting in error!' sys.exit(1) else: print >> sys.stderr, 'I must continue on...' pathname = path.split(single_patch.name)[0] for i in range(len(unapplied_patch_loc)): if opt.apply_patch and unapplied_patch_loc[i] in pathname: print ' Moving %s to %s' % (single_patch.name, applied_patch_loc[i]) single_patch.move(applied_patch_loc[i], silent=True) elif opt.reverse_patch and applied_patch_loc[i] in pathname: print ' Moving %s to %s' % (single_patch.name, unapplied_patch_loc[i]) single_patch.move(unapplied_patch_loc[i], silent=True) # If we successfully applied the patch, see if we can't figure out # where it's supposed to be based on its path else: print 'Patch %s successfully applied' % single_patch.name pathname = path.split(single_patch.name)[0] for i in range(len(unapplied_patch_loc)): if opt.apply_patch and unapplied_patch_loc[i] in pathname: print ' Moving %s to %s' % (single_patch.name, applied_patch_loc[i]) single_patch.move(applied_patch_loc[i], silent=True) elif opt.reverse_patch and applied_patch_loc[i] in pathname: print ' Moving %s to %s' % (single_patch.name, unapplied_patch_loc[i]) single_patch.move(unapplied_patch_loc[i], silent=True) sys.exit(0) #~~~~~~~~~~~ Check our internet access for remaining functionality ~~~~~~~~~ # Set the default timeout socket.setdefaulttimeout(opt.timeout) # Make sure we have internet access, since we'll need it from now on # try: # amberhome = urlopen('http://ambermd.org') # amberhome.close() # del amberhome # except IOError, err: # if 'timed out' in str(err): # print >> sys.stderr, \ # 'Error: Could not connect to http://ambermd.org in %.2f seconds' % ( # opt.timeout) # else: # print >> sys.stderr, 'Error: No internet access!' # sys.exit(1) # Now that we've demonstrated internet functionality, I'll remove the timeout # and let patches download for any length of time. Should this be set to some # large-ish number? socket.setdefaulttimeout(None) #~~~~~~~~~~ Compile list of unapplied patches by peeking online ~~~~~~~~~~~~~ # Loop over every bugfix repo we're going to look for fixes from unapplied_patches = [[] for i in range(len(applied_patch_loc))] unapplied_patch_nums = [[] for i in range(len(applied_patch_loc))] for i in range(len(applied_patch_loc)): if not check_repo[i]: continue # skip any repos we shouldn't check patchnum = last_patch_applied[i] + 1 patch_desc = patch_description(patch_locs[i], patchnum, opt.full_desc) while patch_desc: unapplied_patches[i].append(patch_desc) unapplied_patch_nums[i].append(patchnum) patchnum += 1 patch_desc = patch_description(patch_locs[i], patchnum, opt.full_desc) #~~~~~ Code block for --check-updates (using list from last section ) ~~~~~~ # If we just wanted to check the updates, just print out the descriptions now if opt.check_updates: return_code = 0 summary_string = '' for i in range(len(applied_patch_loc)): if not check_repo[i]: continue # skip any repos we shouldn't check if not unapplied_patches[i]: print '%s is up to date' % patch_desc_list[i] continue # If we make it here, that means we have unapplied patches return_code = 2 summary_string += '%s has %d unapplied patches: %s\n' % (patch_desc_list[i], len(unapplied_patches[i]), ', '.join([str(j) for j in unapplied_patch_nums[i]])) if not opt.silent: for toprint in unapplied_patches[i]: print toprint # Now exit with an exit code equal to the number of updated files we still # need. This is for scripting purposes print summary_string sys.exit(return_code) #~~~~ Code block for --download-patches (and beginning of --update-tree) ~~~~~ # If we want to apply the patches or just download them, do it now if opt.download_patches or opt.update_tree: patch_list = [[] for i in range(len(unapplied_patch_loc))] for i in range(len(patch_locs)): if not check_repo[i]: continue # skip any repos we shouldn't check print 'Determining patches to download for %s' % patch_desc_list[i] for num in unapplied_patch_nums[i]: patch_list[i].append(download_patch(patch_locs[i], num, unapplied_patch_loc[i])) if patch_list[i]: print '' else: print '%s is up to date. Nothing to download\n' % patch_desc_list[i] if sum([len(l) for l in patch_list]) == 0: sys.exit(0) # Print the details about the patches we're about to apply for i in range(len(patch_locs)): if not check_repo[i]: continue # skip any repos we shouldn't check if patch_list[i] and not opt.silent: print 'Downloaded patch files for ', patch_desc_list[i], ':' for ipatch in patch_list[i]: describe_patch(ipatch, opt.full_desc) print '' # If we just wanted to download them, quit out now if opt.download_patches: sys.exit(0) #~~~~~~~~~~~~~~~ Rest of code block for --update-tree ~~~~~~~~~~~~~~~~~~~~~~~ # Now the only thing that's left to do is apply the patches. We will apply # the patches one by one, each time checking all of the files that are # going to be modified by this patch. If all of the files we are going to # modify exist in our tree, then go ahead and execute the patch. If none # of the files are present, make a note of it and go to the next patch. # SPECIAL CASE. If our patch modifies *this* script, we will execute that # patch, and then immediately quit! This allows this program to fix itself, # and we have to assume that any bug fix that comes *after* the bugfix that # patches this file requires the updated/patched version of this program. # This is VERY IMPORTANT if this script is to be editable. # Loop through each patch repository for i in range(len(patch_list)): if not check_repo[i]: continue # skip any repos we shouldn't check # Loop through each new patch in each repository print 'Applying %s patches' % patch_desc_list[i] for ipatch in patch_list[i]: check = _files_present(ipatch) # If check == -1, then we have some files present, others not (BAD) # If check == 0, then no files are present # If check == 1, then all files are present # If check == 2, THIS script is patched if check == 0: print ('Skipping patch %s. You do not have the package containing' + ' the modified files.') % (ipatch.name.strip('.patches/')) # Move this patch over so we don't keep asking about it ipatch.move(applied_patch_loc[i]) continue elif check == -1 or check == -2: if opt.ignore_fails: print >> sys.stderr, ('Warning: %s is trying to patch some ' + 'nonexistent files. Parts will fail.') % ( ipatch.name.strip('.patches/')) else: print >> sys.stderr, ('Error: %s is trying to patch some ' + 'nonexistent files. Complain to amber@ambermd.org ' + 'or try --ignore-fails and soldier on') % ( ipatch.name.strip('.patches/')) sys.exit(1) if ipatch.apply_patch() and not opt.ignore_fails: sys.exit(1) ipatch.move(applied_patch_loc[i]) elif check == 1: if ipatch.apply_patch() and not opt.ignore_fails: sys.exit(1) ipatch.move(applied_patch_loc[i]) elif check == 2: print ('This patch patches %s. I will quit after this patch so ' + 'the rest may be applied with the updated version.') % ( path.split(sys.argv[0])[1]) if ipatch.apply_patch() and not opt.ignore_fails: sys.exit(1) ipatch.move(applied_patch_loc[i]) sys.exit(0) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~