tools/update-packaging/make_incremental_updates.py

Thu, 15 Jan 2015 15:59:08 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 15:59:08 +0100
branch
TOR_BUG_9701
changeset 10
ac0c01689b40
permissions
-rwxr-xr-x

Implement a real Private Browsing Mode condition by changing the API/ABI;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     1 # This Source Code Form is subject to the terms of the Mozilla Public
     2 # License, v. 2.0. If a copy of the MPL was not distributed with this
     3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     5 import os
     6 import shutil
     7 import sha
     8 from os.path import join, getsize
     9 from stat import *
    10 import re
    11 import sys
    12 import getopt
    13 import time
    14 import datetime
    15 import bz2
    16 import string
    17 import tempfile
    19 class PatchInfo:
    20     """ Represents the meta-data associated with a patch
    21         work_dir = working dir where files are stored for this patch
    22         archive_files = list of files to include in this patch
    23         manifestv2 = set of manifest version 2 patch instructions
    24         manifestv3 = set of manifest version 3 patch instructions
    25         file_exclusion_list = 
    26         files to exclude from this patch. names without slashes will be
    27         excluded anywhere in the directory hiearchy.   names with slashes
    28         will only be excluded at that exact path
    29         """
    30     def __init__(self, work_dir, file_exclusion_list, path_exclusion_list):
    31         self.work_dir=work_dir
    32         self.archive_files=[]
    33         self.manifestv2=[]
    34         self.manifestv3=[]
    35         self.file_exclusion_list=file_exclusion_list
    36         self.path_exclusion_list=path_exclusion_list
    38     def append_add_instruction(self, filename):
    39         """ Appends an add instruction for this patch.
    40             if filename starts with distribution/extensions/.*/ this will add an
    41             add-if instruction that will add the file if the parent directory
    42             of the file exists.  This was ported from
    43             mozilla/tools/update-packaging/common.sh's make_add_instruction.
    44         """
    45         m = re.match("((?:|.*/)distribution/extensions/.*)/", filename)
    46         if m:
    47             # Directory immediately following extensions is used for the test
    48             testdir = m.group(1)
    49             print '     add-if "'+testdir+'" "'+filename+'"'
    50             self.manifestv2.append('add-if "'+testdir+'" "'+filename+'"')
    51             self.manifestv3.append('add-if "'+testdir+'" "'+filename+'"')
    52         else:
    53             print '        add "'+filename+'"'
    54             self.manifestv2.append('add "'+filename+'"')
    55             self.manifestv3.append('add "'+filename+'"')
    57     def append_add_if_not_instruction(self, filename):
    58         """ Appends an add-if-not instruction to the version 3 manifest for this patch.
    59             This was ported from mozilla/tools/update-packaging/common.sh's
    60             make_add_if_not_instruction.
    61         """
    62         print ' add-if-not "'+filename+'" "'+filename+'"'
    63         self.manifestv3.append('add-if-not "'+filename+'" "'+filename+'"')
    65     def append_patch_instruction(self, filename, patchname):
    66         """ Appends a patch instruction for this patch.
    68             filename = file to patch
    69             patchname = patchfile to apply to file
    71             if filename starts with distribution/extensions/.*/ this will add a
    72             patch-if instruction that will patch the file if the parent
    73             directory of the file exists. This was ported from
    74             mozilla/tools/update-packaging/common.sh's make_patch_instruction.
    75         """
    76         m = re.match("((?:|.*/)distribution/extensions/.*)/", filename)
    77         if m:
    78             testdir = m.group(1)
    79             print '   patch-if "'+testdir+'" "'+patchname+'" "'+filename+'"'
    80             self.manifestv2.append('patch-if "'+testdir+'" "'+patchname+'" "'+filename+'"')
    81             self.manifestv3.append('patch-if "'+testdir+'" "'+patchname+'" "'+filename+'"')
    82         else:
    83             print '      patch "'+patchname+'" "'+filename+'"'
    84             self.manifestv2.append('patch "'+patchname+'" "'+filename+'"')
    85             self.manifestv3.append('patch "'+patchname+'" "'+filename+'"')
    87     def append_remove_instruction(self, filename):
    88         """ Appends an remove instruction for this patch.
    89             This was ported from
    90             mozilla/tools/update-packaging/common.sh/make_remove_instruction
    91         """
    92         if filename.endswith("/"):
    93             print '      rmdir "'+filename+'"'
    94             self.manifestv2.append('rmdir "'+filename+'"')
    95             self.manifestv3.append('rmdir "'+filename+'"')
    96         elif filename.endswith("/*"):
    97             filename = filename[:-1]
    98             print '    rmrfdir "'+filename+'"'
    99             self.manifestv2.append('rmrfdir "'+filename+'"')
   100             self.manifestv3.append('rmrfdir "'+filename+'"')
   101         else:
   102             print '     remove "'+filename+'"'
   103             self.manifestv2.append('remove "'+filename+'"')
   104             self.manifestv3.append('remove "'+filename+'"')
   106     def create_manifest_files(self):
   107         """ Create the v2 manifest file in the root of the work_dir """
   108         manifest_file_path = os.path.join(self.work_dir,"updatev2.manifest")
   109         manifest_file = open(manifest_file_path, "wb")
   110         manifest_file.writelines("type \"partial\"\n")
   111         manifest_file.writelines(string.join(self.manifestv2, '\n'))
   112         manifest_file.writelines("\n")
   113         manifest_file.close()
   115         bzip_file(manifest_file_path)
   116         self.archive_files.append('"updatev2.manifest"')
   118         """ Create the v3 manifest file in the root of the work_dir """
   119         manifest_file_path = os.path.join(self.work_dir,"updatev3.manifest")
   120         manifest_file = open(manifest_file_path, "wb")
   121         manifest_file.writelines("type \"partial\"\n")
   122         manifest_file.writelines(string.join(self.manifestv3, '\n'))
   123         manifest_file.writelines("\n")
   124         manifest_file.close()
   126         bzip_file(manifest_file_path)
   127         self.archive_files.append('"updatev3.manifest"')
   129     def build_marfile_entry_hash(self, root_path):
   130         """ Iterates through the root_path, creating a MarFileEntry for each file
   131             and directory in that path.  Excludes any filenames in the file_exclusion_list
   132         """
   133         mar_entry_hash = {}
   134         filename_set = set()
   135         dirname_set = set()
   136         for root, dirs, files in os.walk(root_path):
   137             for name in files:
   138                 # filename is the relative path from root directory
   139                 partial_path = root[len(root_path)+1:]
   140                 if name not in self.file_exclusion_list:
   141                     filename = os.path.join(partial_path, name)
   142                     if "/"+filename not in self.path_exclusion_list:
   143                         mar_entry_hash[filename]=MarFileEntry(root_path, filename)
   144                         filename_set.add(filename)
   146             for name in dirs:
   147                 # dirname is the relative path from root directory
   148                 partial_path = root[len(root_path)+1:]
   149                 if name not in self.file_exclusion_list:
   150                     dirname = os.path.join(partial_path, name)
   151                     if "/"+dirname not in self.path_exclusion_list:
   152                         dirname = dirname+"/"
   153                         mar_entry_hash[dirname]=MarFileEntry(root_path, dirname)
   154                         dirname_set.add(dirname)
   156         return mar_entry_hash, filename_set, dirname_set
   159 class MarFileEntry:
   160     """Represents a file inside a Mozilla Archive Format (MAR)
   161         abs_path = abspath to the the file
   162         name =  relative path within the mar.  e.g.
   163           foo.mar/dir/bar.txt extracted into /tmp/foo:
   164             abs_path=/tmp/foo/dir/bar.txt
   165             name = dir/bar.txt
   166     """ 
   167     def __init__(self, root, name):
   168         """root = path the the top of the mar
   169            name = relative path within the mar"""
   170         self.name=name.replace("\\", "/")
   171         self.abs_path=os.path.join(root,name)
   172         self.sha_cache=None
   174     def __str__(self):
   175         return 'Name: %s FullPath: %s' %(self.name,self.abs_path)
   177     def calc_file_sha_digest(self, filename):
   178         """ Returns sha digest of given filename"""
   179         file_content = open(filename, 'r').read()
   180         return sha.new(file_content).digest()
   182     def sha(self):
   183         """ Returns sha digest of file repreesnted by this _marfile_entry"""
   184         if not self.sha_cache:
   185             self.sha_cache=self.calc_file_sha_digest(self.abs_path)
   186         return self.sha_cache
   188 def exec_shell_cmd(cmd):
   189     """Execs shell cmd and raises an exception if the cmd fails"""
   190     if (os.system(cmd)):
   191         raise Exception, "cmd failed "+cmd
   194 def copy_file(src_file_abs_path, dst_file_abs_path):
   195     """ Copies src to dst creating any parent dirs required in dst first """
   196     dst_file_dir=os.path.dirname(dst_file_abs_path)
   197     if not os.path.exists(dst_file_dir):
   198          os.makedirs(dst_file_dir)
   199     # Copy the file over
   200     shutil.copy2(src_file_abs_path, dst_file_abs_path)
   202 def bzip_file(filename):
   203     """ Bzip's the file in place.  The original file is replaced with a bzip'd version of itself
   204         assumes the path is absolute"""
   205     exec_shell_cmd('bzip2 -z9 "' + filename+'"')
   206     os.rename(filename+".bz2",filename)
   208 def bunzip_file(filename):
   209     """ Bzip's the file in palce.   The original file is replaced with a bunzip'd version of itself.
   210         doesn't matter if the filename ends in .bz2 or not"""
   211     if not filename.endswith(".bz2"):
   212         os.rename(filename, filename+".bz2")
   213         filename=filename+".bz2"
   214     exec_shell_cmd('bzip2 -d "' + filename+'"') 
   217 def extract_mar(filename, work_dir): 
   218     """ Extracts the marfile intot he work_dir
   219         assumes work_dir already exists otherwise will throw osError"""
   220     print "Extracting "+filename+" to "+work_dir
   221     saved_path = os.getcwd()
   222     try:
   223         os.chdir(work_dir)
   224         exec_shell_cmd("mar -x "+filename)
   225     finally:
   226         os.chdir(saved_path)
   228 def create_partial_patch_for_file(from_marfile_entry, to_marfile_entry, shas, patch_info):
   229     """ Creates the partial patch file and manifest entry for the pair of files passed in
   230     """
   231     if not (from_marfile_entry.sha(),to_marfile_entry.sha()) in shas:
   232         print 'diffing "'+from_marfile_entry.name+'\"'
   233         #bunzip to/from
   234         bunzip_file(from_marfile_entry.abs_path)
   235         bunzip_file(to_marfile_entry.abs_path)
   237         # The patch file will be created in the working directory with the
   238         # name of the file in the mar + .patch
   239         patch_file_abs_path = os.path.join(patch_info.work_dir,from_marfile_entry.name+".patch")
   240         patch_file_dir=os.path.dirname(patch_file_abs_path)
   241         if not os.path.exists(patch_file_dir):
   242             os.makedirs(patch_file_dir)
   244         # Create bzip'd patch file
   245         exec_shell_cmd("mbsdiff "+from_marfile_entry.abs_path+" "+to_marfile_entry.abs_path+" "+patch_file_abs_path)
   246         bzip_file(patch_file_abs_path)
   248         # Create bzip's full file
   249         full_file_abs_path =  os.path.join(patch_info.work_dir, to_marfile_entry.name)
   250         shutil.copy2(to_marfile_entry.abs_path, full_file_abs_path)
   251         bzip_file(full_file_abs_path)
   253         if os.path.getsize(patch_file_abs_path) < os.path.getsize(full_file_abs_path):
   254             # Patch is smaller than file.  Remove the file and add patch to manifest
   255             os.remove(full_file_abs_path)
   256             file_in_manifest_name = from_marfile_entry.name+".patch"
   257             file_in_manifest_abspath = patch_file_abs_path
   258             patch_info.append_patch_instruction(to_marfile_entry.name, file_in_manifest_name)
   259         else:
   260             # File is smaller than patch.  Remove the patch and add file to manifest
   261             os.remove(patch_file_abs_path)
   262             file_in_manifest_name = from_marfile_entry.name
   263             file_in_manifest_abspath = full_file_abs_path
   264             patch_info.append_add_instruction(file_in_manifest_name)
   266         shas[from_marfile_entry.sha(),to_marfile_entry.sha()] = (file_in_manifest_name,file_in_manifest_abspath)
   267         patch_info.archive_files.append('"'+file_in_manifest_name+'"')
   268     else:
   269         filename, src_file_abs_path = shas[from_marfile_entry.sha(),to_marfile_entry.sha()]
   270         # We've already calculated the patch for this pair of files.
   271         if (filename.endswith(".patch")):
   272             # print "skipping diff: "+from_marfile_entry.name
   273             # Patch was smaller than file - add patch instruction to manifest
   274             file_in_manifest_name = to_marfile_entry.name+'.patch';
   275             patch_info.append_patch_instruction(to_marfile_entry.name, file_in_manifest_name)
   276         else:
   277             # File was smaller than file - add file to manifest
   278             file_in_manifest_name = to_marfile_entry.name
   279             patch_info.append_add_instruction(file_in_manifest_name)
   280         # Copy the pre-calculated file into our new patch work aread
   281         copy_file(src_file_abs_path, os.path.join(patch_info.work_dir, file_in_manifest_name))
   282         patch_info.archive_files.append('"'+file_in_manifest_name+'"')
   284 def create_add_patch_for_file(to_marfile_entry, patch_info):
   285     """  Copy the file to the working dir, add the add instruction, and add it to the list of archive files """
   286     copy_file(to_marfile_entry.abs_path, os.path.join(patch_info.work_dir, to_marfile_entry.name))
   287     patch_info.append_add_instruction(to_marfile_entry.name)
   288     patch_info.archive_files.append('"'+to_marfile_entry.name+'"')
   290 def create_add_if_not_patch_for_file(to_marfile_entry, patch_info):
   291     """  Copy the file to the working dir, add the add-if-not instruction, and add it to the list of archive files """
   292     copy_file(to_marfile_entry.abs_path, os.path.join(patch_info.work_dir, to_marfile_entry.name))
   293     patch_info.append_add_if_not_instruction(to_marfile_entry.name)
   294     patch_info.archive_files.append('"'+to_marfile_entry.name+'"')
   296 def process_explicit_remove_files(dir_path, patch_info): 
   297     """ Looks for a 'removed-files' file in the dir_path.  If the removed-files does not exist
   298     this will throw.  If found adds the removed-files
   299     found in that file to the patch_info"""
   301     # Windows and linux have this file at the root of the dir
   302     list_file_path = os.path.join(dir_path, "removed-files")
   303     prefix=""
   304     if not os.path.exists(list_file_path):
   305         # On Mac removed-files contains relative paths from Contents/MacOS/
   306         prefix= "Contents/MacOS"
   307         list_file_path = os.path.join(dir_path, prefix+"/removed-files")
   309     if (os.path.exists(list_file_path)):
   310         list_file = bz2.BZ2File(list_file_path,"r") # throws if doesn't exist
   312         lines = []
   313         for line in list_file:
   314             lines.append(line.strip())
   316         lines.sort(reverse=True)
   317         for line in lines:
   318             # Exclude any blank and comment lines.
   319             if line and not line.startswith("#"):
   320                 if prefix != "":
   321                     if line.startswith("../"):
   322                         line = line.replace("../../", "")
   323                         line = line.replace("../", "Contents/")
   324                     else:
   325                         line = os.path.join(prefix,line)
   326                 # Python on windows uses \ for path separators and the update
   327                 # manifests expects / for path separators on all platforms.
   328                 line = line.replace("\\", "/")
   329                 patch_info.append_remove_instruction(line)
   331 def create_partial_patch(from_dir_path, to_dir_path, patch_filename, shas, patch_info, forced_updates, add_if_not_list):
   332     """ Builds a partial patch by comparing the files in from_dir_path to those of to_dir_path"""
   333     # Cannocolize the paths for safey
   334     from_dir_path = os.path.abspath(from_dir_path)
   335     to_dir_path = os.path.abspath(to_dir_path)
   336     # Create a hashtable of the from  and to directories
   337     from_dir_hash,from_file_set,from_dir_set = patch_info.build_marfile_entry_hash(from_dir_path)
   338     to_dir_hash,to_file_set,to_dir_set = patch_info.build_marfile_entry_hash(to_dir_path)
   339     # Require that the precomplete file is included in the complete update
   340     if "precomplete" not in to_file_set:
   341         raise Exception, "missing precomplete file in: "+to_dir_path
   342     # Create a list of the forced updates 
   343     forced_list = forced_updates.strip().split('|')
   344     forced_list.append("precomplete")
   346     # Files which exist in both sets need to be patched
   347     patch_filenames = list(from_file_set.intersection(to_file_set))
   348     patch_filenames.sort(reverse=True)
   349     for filename in patch_filenames:
   350         from_marfile_entry = from_dir_hash[filename]
   351         to_marfile_entry = to_dir_hash[filename]
   352         if os.path.basename(filename) in add_if_not_list:
   353             # This filename is in the add if not list, explicitly add-if-not
   354             create_add_if_not_patch_for_file(to_dir_hash[filename], patch_info)
   355         elif filename in forced_list:
   356             print 'Forcing "'+filename+'"'
   357             # This filename is in the forced list, explicitly add
   358             create_add_patch_for_file(to_dir_hash[filename], patch_info)
   359         else: 
   360           if from_marfile_entry.sha() != to_marfile_entry.sha():
   361               # Not the same - calculate a patch
   362               create_partial_patch_for_file(from_marfile_entry, to_marfile_entry, shas, patch_info)
   364     # files in to_dir not in from_dir need to added
   365     add_filenames = list(to_file_set - from_file_set)
   366     add_filenames.sort(reverse=True)
   367     for filename in add_filenames:
   368         if os.path.basename(filename) in add_if_not_list:
   369             create_add_if_not_patch_for_file(to_dir_hash[filename], patch_info)
   370         else:
   371             create_add_patch_for_file(to_dir_hash[filename], patch_info)
   373     # files in from_dir not in to_dir need to be removed
   374     remove_filenames = list(from_file_set - to_file_set)
   375     remove_filenames.sort(reverse=True)
   376     for filename in remove_filenames:
   377         patch_info.append_remove_instruction(from_dir_hash[filename].name)
   379     process_explicit_remove_files(to_dir_path, patch_info)
   381     # directories in from_dir not in to_dir need to be removed
   382     remove_dirnames = list(from_dir_set - to_dir_set)
   383     remove_dirnames.sort(reverse=True)
   384     for dirname in remove_dirnames:
   385         patch_info.append_remove_instruction(from_dir_hash[dirname].name)
   387     # Construct the Manifest files
   388     patch_info.create_manifest_files()
   390     # And construct the mar
   391     mar_cmd = 'mar -C '+patch_info.work_dir+' -c output.mar '+string.join(patch_info.archive_files, ' ')
   392     exec_shell_cmd(mar_cmd)
   394     # Copy mar to final destination
   395     patch_file_dir = os.path.split(patch_filename)[0]
   396     if not os.path.exists(patch_file_dir):
   397         os.makedirs(patch_file_dir)
   398     shutil.copy2(os.path.join(patch_info.work_dir,"output.mar"), patch_filename)
   399     return patch_filename
   401 def usage():
   402     print "-h for help"
   403     print "-f for patchlist_file"
   405 def get_buildid(work_dir, platform):
   406     """ extracts buildid from MAR
   407         TODO: this should handle 1.8 branch too
   408     """
   409     if platform == 'mac':
   410       ini = '%s/Contents/MacOS/application.ini' % work_dir
   411     else:
   412       ini = '%s/application.ini' % work_dir
   413     if not os.path.exists(ini):
   414         print 'WARNING: application.ini not found, cannot find build ID'
   415         return ''
   416     file = bz2.BZ2File(ini)
   417     for line in file:
   418       if line.find('BuildID') == 0:
   419         return line.strip().split('=')[1]
   420     print 'WARNING: cannot find build ID in application.ini'
   421     return ''
   423 def decode_filename(filepath):
   424     """ Breaks filename/dir structure into component parts based on regex
   425         for example: firefox-3.0b3pre.en-US.linux-i686.complete.mar
   426         Or linux-i686/en-US/firefox-3.0b3.complete.mar
   427         Returns dict with keys product, version, locale, platform, type
   428     """
   429     try:
   430       m = re.search(
   431         '(?P<product>\w+)(-)(?P<version>\w+\.\w+(\.\w+){0,2})(\.)(?P<locale>.+?)(\.)(?P<platform>.+?)(\.)(?P<type>\w+)(.mar)',
   432       os.path.basename(filepath))
   433       return m.groupdict()
   434     except Exception, exc:
   435       try:
   436         m = re.search(
   437           '(?P<platform>.+?)\/(?P<locale>.+?)\/(?P<product>\w+)-(?P<version>\w+\.\w+)\.(?P<type>\w+).mar',
   438         filepath)
   439         return m.groupdict()
   440       except:
   441         raise Exception("could not parse filepath %s: %s" % (filepath, exc))
   443 def create_partial_patches(patches):
   444     """ Given the patches generates a set of partial patches"""
   445     shas = {}
   447     work_dir_root = None
   448     metadata = []
   449     try:
   450         work_dir_root = tempfile.mkdtemp('-fastmode', 'tmp', os.getcwd())
   451         print "Building patches using work dir: %s" % (work_dir_root)
   453         # Iterate through every patch set in the patch file
   454         patch_num = 1
   455         for patch in patches:
   456             startTime = time.time()
   458             from_filename,to_filename,patch_filename,forced_updates = patch.split(",")
   459             from_filename,to_filename,patch_filename = os.path.abspath(from_filename),os.path.abspath(to_filename),os.path.abspath(patch_filename)
   461             # Each patch iteration uses its own work dir
   462             work_dir = os.path.join(work_dir_root,str(patch_num))
   463             os.mkdir(work_dir)
   465             # Extract from mar into from dir
   466             work_dir_from =  os.path.join(work_dir,"from");
   467             os.mkdir(work_dir_from)
   468             extract_mar(from_filename,work_dir_from)
   469             from_decoded = decode_filename(from_filename)
   470             from_buildid = get_buildid(work_dir_from, from_decoded['platform'])
   471             from_shasum = sha.sha(open(from_filename).read()).hexdigest()
   472             from_size = str(os.path.getsize(to_filename))
   474             # Extract to mar into to dir
   475             work_dir_to =  os.path.join(work_dir,"to")
   476             os.mkdir(work_dir_to)
   477             extract_mar(to_filename, work_dir_to)
   478             to_decoded = decode_filename(from_filename)
   479             to_buildid = get_buildid(work_dir_to, to_decoded['platform'])
   480             to_shasum = sha.sha(open(to_filename).read()).hexdigest()
   481             to_size = str(os.path.getsize(to_filename))
   483             mar_extract_time = time.time()
   485             partial_filename = create_partial_patch(work_dir_from, work_dir_to, patch_filename, shas, PatchInfo(work_dir, ['update.manifest','updatev2.manifest','updatev3.manifest','removed-files'],['/readme.txt']),forced_updates,['channel-prefs.js','update-settings.ini'])
   486             partial_buildid = to_buildid
   487             partial_shasum = sha.sha(open(partial_filename).read()).hexdigest()
   488             partial_size = str(os.path.getsize(partial_filename))
   490             metadata.append({
   491              'to_filename': os.path.basename(to_filename),
   492              'from_filename': os.path.basename(from_filename),
   493              'partial_filename': os.path.basename(partial_filename),
   494              'to_buildid':to_buildid, 
   495              'from_buildid':from_buildid, 
   496              'to_sha1sum':to_shasum, 
   497              'from_sha1sum':from_shasum, 
   498              'partial_sha1sum':partial_shasum, 
   499              'to_size':to_size,
   500              'from_size':from_size,
   501              'partial_size':partial_size,
   502              'to_version':to_decoded['version'],
   503              'from_version':from_decoded['version'],
   504              'locale':from_decoded['locale'],
   505              'platform':from_decoded['platform'],
   506             })
   507             print "done with patch %s/%s time (%.2fs/%.2fs/%.2fs) (mar/patch/total)" % (str(patch_num),str(len(patches)),mar_extract_time-startTime,time.time()-mar_extract_time,time.time()-startTime)
   508             patch_num += 1
   509         return metadata
   510     finally:
   511         # If we fail or get a ctrl-c during run be sure to clean up temp dir
   512         if (work_dir_root and os.path.exists(work_dir_root)):
   513             shutil.rmtree(work_dir_root)
   515 def main(argv):
   516     patchlist_file = None
   517     try:
   518          opts, args = getopt.getopt(argv, "hf:", ["help", "patchlist_file="])
   519          for opt, arg in opts:
   520             if opt in ("-h", "--help"):
   521                 usage()
   522                 sys.exit()
   523             elif opt in ("-f", "--patchlist_file"):
   524                 patchlist_file = arg
   525     except getopt.GetoptError:
   526           usage()
   527           sys.exit(2)
   529     if not patchlist_file:
   530         usage()
   531         sys.exit(2)
   533     patches = []
   534     f = open(patchlist_file, 'r')
   535     for line in f.readlines():
   536         patches.append(line)
   537     f.close()
   538     create_partial_patches(patches)
   540 if __name__ == "__main__":
   541     main(sys.argv[1:])

mercurial