tools/update-packaging/make_incremental_updates.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/tools/update-packaging/make_incremental_updates.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,542 @@
     1.4 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.7 +
     1.8 +import os
     1.9 +import shutil
    1.10 +import sha
    1.11 +from os.path import join, getsize
    1.12 +from stat import *
    1.13 +import re
    1.14 +import sys
    1.15 +import getopt
    1.16 +import time
    1.17 +import datetime
    1.18 +import bz2
    1.19 +import string
    1.20 +import tempfile
    1.21 +
    1.22 +class PatchInfo:
    1.23 +    """ Represents the meta-data associated with a patch
    1.24 +        work_dir = working dir where files are stored for this patch
    1.25 +        archive_files = list of files to include in this patch
    1.26 +        manifestv2 = set of manifest version 2 patch instructions
    1.27 +        manifestv3 = set of manifest version 3 patch instructions
    1.28 +        file_exclusion_list = 
    1.29 +        files to exclude from this patch. names without slashes will be
    1.30 +        excluded anywhere in the directory hiearchy.   names with slashes
    1.31 +        will only be excluded at that exact path
    1.32 +        """
    1.33 +    def __init__(self, work_dir, file_exclusion_list, path_exclusion_list):
    1.34 +        self.work_dir=work_dir
    1.35 +        self.archive_files=[]
    1.36 +        self.manifestv2=[]
    1.37 +        self.manifestv3=[]
    1.38 +        self.file_exclusion_list=file_exclusion_list
    1.39 +        self.path_exclusion_list=path_exclusion_list
    1.40 +
    1.41 +    def append_add_instruction(self, filename):
    1.42 +        """ Appends an add instruction for this patch.
    1.43 +            if filename starts with distribution/extensions/.*/ this will add an
    1.44 +            add-if instruction that will add the file if the parent directory
    1.45 +            of the file exists.  This was ported from
    1.46 +            mozilla/tools/update-packaging/common.sh's make_add_instruction.
    1.47 +        """
    1.48 +        m = re.match("((?:|.*/)distribution/extensions/.*)/", filename)
    1.49 +        if m:
    1.50 +            # Directory immediately following extensions is used for the test
    1.51 +            testdir = m.group(1)
    1.52 +            print '     add-if "'+testdir+'" "'+filename+'"'
    1.53 +            self.manifestv2.append('add-if "'+testdir+'" "'+filename+'"')
    1.54 +            self.manifestv3.append('add-if "'+testdir+'" "'+filename+'"')
    1.55 +        else:
    1.56 +            print '        add "'+filename+'"'
    1.57 +            self.manifestv2.append('add "'+filename+'"')
    1.58 +            self.manifestv3.append('add "'+filename+'"')
    1.59 +
    1.60 +    def append_add_if_not_instruction(self, filename):
    1.61 +        """ Appends an add-if-not instruction to the version 3 manifest for this patch.
    1.62 +            This was ported from mozilla/tools/update-packaging/common.sh's
    1.63 +            make_add_if_not_instruction.
    1.64 +        """
    1.65 +        print ' add-if-not "'+filename+'" "'+filename+'"'
    1.66 +        self.manifestv3.append('add-if-not "'+filename+'" "'+filename+'"')
    1.67 +
    1.68 +    def append_patch_instruction(self, filename, patchname):
    1.69 +        """ Appends a patch instruction for this patch.
    1.70 +
    1.71 +            filename = file to patch
    1.72 +            patchname = patchfile to apply to file
    1.73 +
    1.74 +            if filename starts with distribution/extensions/.*/ this will add a
    1.75 +            patch-if instruction that will patch the file if the parent
    1.76 +            directory of the file exists. This was ported from
    1.77 +            mozilla/tools/update-packaging/common.sh's make_patch_instruction.
    1.78 +        """
    1.79 +        m = re.match("((?:|.*/)distribution/extensions/.*)/", filename)
    1.80 +        if m:
    1.81 +            testdir = m.group(1)
    1.82 +            print '   patch-if "'+testdir+'" "'+patchname+'" "'+filename+'"'
    1.83 +            self.manifestv2.append('patch-if "'+testdir+'" "'+patchname+'" "'+filename+'"')
    1.84 +            self.manifestv3.append('patch-if "'+testdir+'" "'+patchname+'" "'+filename+'"')
    1.85 +        else:
    1.86 +            print '      patch "'+patchname+'" "'+filename+'"'
    1.87 +            self.manifestv2.append('patch "'+patchname+'" "'+filename+'"')
    1.88 +            self.manifestv3.append('patch "'+patchname+'" "'+filename+'"')
    1.89 +
    1.90 +    def append_remove_instruction(self, filename):
    1.91 +        """ Appends an remove instruction for this patch.
    1.92 +            This was ported from
    1.93 +            mozilla/tools/update-packaging/common.sh/make_remove_instruction
    1.94 +        """
    1.95 +        if filename.endswith("/"):
    1.96 +            print '      rmdir "'+filename+'"'
    1.97 +            self.manifestv2.append('rmdir "'+filename+'"')
    1.98 +            self.manifestv3.append('rmdir "'+filename+'"')
    1.99 +        elif filename.endswith("/*"):
   1.100 +            filename = filename[:-1]
   1.101 +            print '    rmrfdir "'+filename+'"'
   1.102 +            self.manifestv2.append('rmrfdir "'+filename+'"')
   1.103 +            self.manifestv3.append('rmrfdir "'+filename+'"')
   1.104 +        else:
   1.105 +            print '     remove "'+filename+'"'
   1.106 +            self.manifestv2.append('remove "'+filename+'"')
   1.107 +            self.manifestv3.append('remove "'+filename+'"')
   1.108 +
   1.109 +    def create_manifest_files(self):
   1.110 +        """ Create the v2 manifest file in the root of the work_dir """
   1.111 +        manifest_file_path = os.path.join(self.work_dir,"updatev2.manifest")
   1.112 +        manifest_file = open(manifest_file_path, "wb")
   1.113 +        manifest_file.writelines("type \"partial\"\n")
   1.114 +        manifest_file.writelines(string.join(self.manifestv2, '\n'))
   1.115 +        manifest_file.writelines("\n")
   1.116 +        manifest_file.close()
   1.117 +
   1.118 +        bzip_file(manifest_file_path)
   1.119 +        self.archive_files.append('"updatev2.manifest"')
   1.120 +
   1.121 +        """ Create the v3 manifest file in the root of the work_dir """
   1.122 +        manifest_file_path = os.path.join(self.work_dir,"updatev3.manifest")
   1.123 +        manifest_file = open(manifest_file_path, "wb")
   1.124 +        manifest_file.writelines("type \"partial\"\n")
   1.125 +        manifest_file.writelines(string.join(self.manifestv3, '\n'))
   1.126 +        manifest_file.writelines("\n")
   1.127 +        manifest_file.close()
   1.128 +
   1.129 +        bzip_file(manifest_file_path)
   1.130 +        self.archive_files.append('"updatev3.manifest"')
   1.131 +
   1.132 +    def build_marfile_entry_hash(self, root_path):
   1.133 +        """ Iterates through the root_path, creating a MarFileEntry for each file
   1.134 +            and directory in that path.  Excludes any filenames in the file_exclusion_list
   1.135 +        """
   1.136 +        mar_entry_hash = {}
   1.137 +        filename_set = set()
   1.138 +        dirname_set = set()
   1.139 +        for root, dirs, files in os.walk(root_path):
   1.140 +            for name in files:
   1.141 +                # filename is the relative path from root directory
   1.142 +                partial_path = root[len(root_path)+1:]
   1.143 +                if name not in self.file_exclusion_list:
   1.144 +                    filename = os.path.join(partial_path, name)
   1.145 +                    if "/"+filename not in self.path_exclusion_list:
   1.146 +                        mar_entry_hash[filename]=MarFileEntry(root_path, filename)
   1.147 +                        filename_set.add(filename)
   1.148 +
   1.149 +            for name in dirs:
   1.150 +                # dirname is the relative path from root directory
   1.151 +                partial_path = root[len(root_path)+1:]
   1.152 +                if name not in self.file_exclusion_list:
   1.153 +                    dirname = os.path.join(partial_path, name)
   1.154 +                    if "/"+dirname not in self.path_exclusion_list:
   1.155 +                        dirname = dirname+"/"
   1.156 +                        mar_entry_hash[dirname]=MarFileEntry(root_path, dirname)
   1.157 +                        dirname_set.add(dirname)
   1.158 +
   1.159 +        return mar_entry_hash, filename_set, dirname_set
   1.160 + 
   1.161 +
   1.162 +class MarFileEntry:
   1.163 +    """Represents a file inside a Mozilla Archive Format (MAR)
   1.164 +        abs_path = abspath to the the file
   1.165 +        name =  relative path within the mar.  e.g.
   1.166 +          foo.mar/dir/bar.txt extracted into /tmp/foo:
   1.167 +            abs_path=/tmp/foo/dir/bar.txt
   1.168 +            name = dir/bar.txt
   1.169 +    """ 
   1.170 +    def __init__(self, root, name):
   1.171 +        """root = path the the top of the mar
   1.172 +           name = relative path within the mar"""
   1.173 +        self.name=name.replace("\\", "/")
   1.174 +        self.abs_path=os.path.join(root,name)
   1.175 +        self.sha_cache=None
   1.176 +
   1.177 +    def __str__(self):
   1.178 +        return 'Name: %s FullPath: %s' %(self.name,self.abs_path)
   1.179 +
   1.180 +    def calc_file_sha_digest(self, filename):
   1.181 +        """ Returns sha digest of given filename"""
   1.182 +        file_content = open(filename, 'r').read()
   1.183 +        return sha.new(file_content).digest()
   1.184 +
   1.185 +    def sha(self):
   1.186 +        """ Returns sha digest of file repreesnted by this _marfile_entry"""
   1.187 +        if not self.sha_cache:
   1.188 +            self.sha_cache=self.calc_file_sha_digest(self.abs_path)
   1.189 +        return self.sha_cache
   1.190 +
   1.191 +def exec_shell_cmd(cmd):
   1.192 +    """Execs shell cmd and raises an exception if the cmd fails"""
   1.193 +    if (os.system(cmd)):
   1.194 +        raise Exception, "cmd failed "+cmd
   1.195 +
   1.196 +
   1.197 +def copy_file(src_file_abs_path, dst_file_abs_path):
   1.198 +    """ Copies src to dst creating any parent dirs required in dst first """
   1.199 +    dst_file_dir=os.path.dirname(dst_file_abs_path)
   1.200 +    if not os.path.exists(dst_file_dir):
   1.201 +         os.makedirs(dst_file_dir)
   1.202 +    # Copy the file over
   1.203 +    shutil.copy2(src_file_abs_path, dst_file_abs_path)
   1.204 +
   1.205 +def bzip_file(filename):
   1.206 +    """ Bzip's the file in place.  The original file is replaced with a bzip'd version of itself
   1.207 +        assumes the path is absolute"""
   1.208 +    exec_shell_cmd('bzip2 -z9 "' + filename+'"')
   1.209 +    os.rename(filename+".bz2",filename)
   1.210 +
   1.211 +def bunzip_file(filename):
   1.212 +    """ Bzip's the file in palce.   The original file is replaced with a bunzip'd version of itself.
   1.213 +        doesn't matter if the filename ends in .bz2 or not"""
   1.214 +    if not filename.endswith(".bz2"):
   1.215 +        os.rename(filename, filename+".bz2")
   1.216 +        filename=filename+".bz2"
   1.217 +    exec_shell_cmd('bzip2 -d "' + filename+'"') 
   1.218 +
   1.219 +
   1.220 +def extract_mar(filename, work_dir): 
   1.221 +    """ Extracts the marfile intot he work_dir
   1.222 +        assumes work_dir already exists otherwise will throw osError"""
   1.223 +    print "Extracting "+filename+" to "+work_dir
   1.224 +    saved_path = os.getcwd()
   1.225 +    try:
   1.226 +        os.chdir(work_dir)
   1.227 +        exec_shell_cmd("mar -x "+filename)
   1.228 +    finally:
   1.229 +        os.chdir(saved_path)
   1.230 +
   1.231 +def create_partial_patch_for_file(from_marfile_entry, to_marfile_entry, shas, patch_info):
   1.232 +    """ Creates the partial patch file and manifest entry for the pair of files passed in
   1.233 +    """
   1.234 +    if not (from_marfile_entry.sha(),to_marfile_entry.sha()) in shas:
   1.235 +        print 'diffing "'+from_marfile_entry.name+'\"'
   1.236 +        #bunzip to/from
   1.237 +        bunzip_file(from_marfile_entry.abs_path)
   1.238 +        bunzip_file(to_marfile_entry.abs_path)
   1.239 +
   1.240 +        # The patch file will be created in the working directory with the
   1.241 +        # name of the file in the mar + .patch
   1.242 +        patch_file_abs_path = os.path.join(patch_info.work_dir,from_marfile_entry.name+".patch")
   1.243 +        patch_file_dir=os.path.dirname(patch_file_abs_path)
   1.244 +        if not os.path.exists(patch_file_dir):
   1.245 +            os.makedirs(patch_file_dir)
   1.246 +
   1.247 +        # Create bzip'd patch file
   1.248 +        exec_shell_cmd("mbsdiff "+from_marfile_entry.abs_path+" "+to_marfile_entry.abs_path+" "+patch_file_abs_path)
   1.249 +        bzip_file(patch_file_abs_path)
   1.250 +
   1.251 +        # Create bzip's full file
   1.252 +        full_file_abs_path =  os.path.join(patch_info.work_dir, to_marfile_entry.name)
   1.253 +        shutil.copy2(to_marfile_entry.abs_path, full_file_abs_path)
   1.254 +        bzip_file(full_file_abs_path)
   1.255 +
   1.256 +        if os.path.getsize(patch_file_abs_path) < os.path.getsize(full_file_abs_path):
   1.257 +            # Patch is smaller than file.  Remove the file and add patch to manifest
   1.258 +            os.remove(full_file_abs_path)
   1.259 +            file_in_manifest_name = from_marfile_entry.name+".patch"
   1.260 +            file_in_manifest_abspath = patch_file_abs_path
   1.261 +            patch_info.append_patch_instruction(to_marfile_entry.name, file_in_manifest_name)
   1.262 +        else:
   1.263 +            # File is smaller than patch.  Remove the patch and add file to manifest
   1.264 +            os.remove(patch_file_abs_path)
   1.265 +            file_in_manifest_name = from_marfile_entry.name
   1.266 +            file_in_manifest_abspath = full_file_abs_path
   1.267 +            patch_info.append_add_instruction(file_in_manifest_name)
   1.268 +
   1.269 +        shas[from_marfile_entry.sha(),to_marfile_entry.sha()] = (file_in_manifest_name,file_in_manifest_abspath)
   1.270 +        patch_info.archive_files.append('"'+file_in_manifest_name+'"')
   1.271 +    else:
   1.272 +        filename, src_file_abs_path = shas[from_marfile_entry.sha(),to_marfile_entry.sha()]
   1.273 +        # We've already calculated the patch for this pair of files.
   1.274 +        if (filename.endswith(".patch")):
   1.275 +            # print "skipping diff: "+from_marfile_entry.name
   1.276 +            # Patch was smaller than file - add patch instruction to manifest
   1.277 +            file_in_manifest_name = to_marfile_entry.name+'.patch';
   1.278 +            patch_info.append_patch_instruction(to_marfile_entry.name, file_in_manifest_name)
   1.279 +        else:
   1.280 +            # File was smaller than file - add file to manifest
   1.281 +            file_in_manifest_name = to_marfile_entry.name
   1.282 +            patch_info.append_add_instruction(file_in_manifest_name)
   1.283 +        # Copy the pre-calculated file into our new patch work aread
   1.284 +        copy_file(src_file_abs_path, os.path.join(patch_info.work_dir, file_in_manifest_name))
   1.285 +        patch_info.archive_files.append('"'+file_in_manifest_name+'"')
   1.286 + 
   1.287 +def create_add_patch_for_file(to_marfile_entry, patch_info):
   1.288 +    """  Copy the file to the working dir, add the add instruction, and add it to the list of archive files """
   1.289 +    copy_file(to_marfile_entry.abs_path, os.path.join(patch_info.work_dir, to_marfile_entry.name))
   1.290 +    patch_info.append_add_instruction(to_marfile_entry.name)
   1.291 +    patch_info.archive_files.append('"'+to_marfile_entry.name+'"')
   1.292 +
   1.293 +def create_add_if_not_patch_for_file(to_marfile_entry, patch_info):
   1.294 +    """  Copy the file to the working dir, add the add-if-not instruction, and add it to the list of archive files """
   1.295 +    copy_file(to_marfile_entry.abs_path, os.path.join(patch_info.work_dir, to_marfile_entry.name))
   1.296 +    patch_info.append_add_if_not_instruction(to_marfile_entry.name)
   1.297 +    patch_info.archive_files.append('"'+to_marfile_entry.name+'"')
   1.298 +
   1.299 +def process_explicit_remove_files(dir_path, patch_info): 
   1.300 +    """ Looks for a 'removed-files' file in the dir_path.  If the removed-files does not exist
   1.301 +    this will throw.  If found adds the removed-files
   1.302 +    found in that file to the patch_info"""
   1.303 +
   1.304 +    # Windows and linux have this file at the root of the dir
   1.305 +    list_file_path = os.path.join(dir_path, "removed-files")
   1.306 +    prefix=""
   1.307 +    if not os.path.exists(list_file_path):
   1.308 +        # On Mac removed-files contains relative paths from Contents/MacOS/
   1.309 +        prefix= "Contents/MacOS"
   1.310 +        list_file_path = os.path.join(dir_path, prefix+"/removed-files")
   1.311 +
   1.312 +    if (os.path.exists(list_file_path)):
   1.313 +        list_file = bz2.BZ2File(list_file_path,"r") # throws if doesn't exist
   1.314 +
   1.315 +        lines = []
   1.316 +        for line in list_file:
   1.317 +            lines.append(line.strip())
   1.318 +
   1.319 +        lines.sort(reverse=True)
   1.320 +        for line in lines:
   1.321 +            # Exclude any blank and comment lines.
   1.322 +            if line and not line.startswith("#"):
   1.323 +                if prefix != "":
   1.324 +                    if line.startswith("../"):
   1.325 +                        line = line.replace("../../", "")
   1.326 +                        line = line.replace("../", "Contents/")
   1.327 +                    else:
   1.328 +                        line = os.path.join(prefix,line)
   1.329 +                # Python on windows uses \ for path separators and the update
   1.330 +                # manifests expects / for path separators on all platforms.
   1.331 +                line = line.replace("\\", "/")
   1.332 +                patch_info.append_remove_instruction(line)
   1.333 +
   1.334 +def create_partial_patch(from_dir_path, to_dir_path, patch_filename, shas, patch_info, forced_updates, add_if_not_list):
   1.335 +    """ Builds a partial patch by comparing the files in from_dir_path to those of to_dir_path"""
   1.336 +    # Cannocolize the paths for safey
   1.337 +    from_dir_path = os.path.abspath(from_dir_path)
   1.338 +    to_dir_path = os.path.abspath(to_dir_path)
   1.339 +    # Create a hashtable of the from  and to directories
   1.340 +    from_dir_hash,from_file_set,from_dir_set = patch_info.build_marfile_entry_hash(from_dir_path)
   1.341 +    to_dir_hash,to_file_set,to_dir_set = patch_info.build_marfile_entry_hash(to_dir_path)
   1.342 +    # Require that the precomplete file is included in the complete update
   1.343 +    if "precomplete" not in to_file_set:
   1.344 +        raise Exception, "missing precomplete file in: "+to_dir_path
   1.345 +    # Create a list of the forced updates 
   1.346 +    forced_list = forced_updates.strip().split('|')
   1.347 +    forced_list.append("precomplete")
   1.348 +
   1.349 +    # Files which exist in both sets need to be patched
   1.350 +    patch_filenames = list(from_file_set.intersection(to_file_set))
   1.351 +    patch_filenames.sort(reverse=True)
   1.352 +    for filename in patch_filenames:
   1.353 +        from_marfile_entry = from_dir_hash[filename]
   1.354 +        to_marfile_entry = to_dir_hash[filename]
   1.355 +        if os.path.basename(filename) in add_if_not_list:
   1.356 +            # This filename is in the add if not list, explicitly add-if-not
   1.357 +            create_add_if_not_patch_for_file(to_dir_hash[filename], patch_info)
   1.358 +        elif filename in forced_list:
   1.359 +            print 'Forcing "'+filename+'"'
   1.360 +            # This filename is in the forced list, explicitly add
   1.361 +            create_add_patch_for_file(to_dir_hash[filename], patch_info)
   1.362 +        else: 
   1.363 +          if from_marfile_entry.sha() != to_marfile_entry.sha():
   1.364 +              # Not the same - calculate a patch
   1.365 +              create_partial_patch_for_file(from_marfile_entry, to_marfile_entry, shas, patch_info)
   1.366 +
   1.367 +    # files in to_dir not in from_dir need to added
   1.368 +    add_filenames = list(to_file_set - from_file_set)
   1.369 +    add_filenames.sort(reverse=True)
   1.370 +    for filename in add_filenames:
   1.371 +        if os.path.basename(filename) in add_if_not_list:
   1.372 +            create_add_if_not_patch_for_file(to_dir_hash[filename], patch_info)
   1.373 +        else:
   1.374 +            create_add_patch_for_file(to_dir_hash[filename], patch_info)
   1.375 +
   1.376 +    # files in from_dir not in to_dir need to be removed
   1.377 +    remove_filenames = list(from_file_set - to_file_set)
   1.378 +    remove_filenames.sort(reverse=True)
   1.379 +    for filename in remove_filenames:
   1.380 +        patch_info.append_remove_instruction(from_dir_hash[filename].name)
   1.381 +
   1.382 +    process_explicit_remove_files(to_dir_path, patch_info)
   1.383 +
   1.384 +    # directories in from_dir not in to_dir need to be removed
   1.385 +    remove_dirnames = list(from_dir_set - to_dir_set)
   1.386 +    remove_dirnames.sort(reverse=True)
   1.387 +    for dirname in remove_dirnames:
   1.388 +        patch_info.append_remove_instruction(from_dir_hash[dirname].name)
   1.389 +
   1.390 +    # Construct the Manifest files
   1.391 +    patch_info.create_manifest_files()
   1.392 +
   1.393 +    # And construct the mar
   1.394 +    mar_cmd = 'mar -C '+patch_info.work_dir+' -c output.mar '+string.join(patch_info.archive_files, ' ')
   1.395 +    exec_shell_cmd(mar_cmd)
   1.396 +
   1.397 +    # Copy mar to final destination
   1.398 +    patch_file_dir = os.path.split(patch_filename)[0]
   1.399 +    if not os.path.exists(patch_file_dir):
   1.400 +        os.makedirs(patch_file_dir)
   1.401 +    shutil.copy2(os.path.join(patch_info.work_dir,"output.mar"), patch_filename)
   1.402 +    return patch_filename
   1.403 +
   1.404 +def usage():
   1.405 +    print "-h for help"
   1.406 +    print "-f for patchlist_file"
   1.407 +
   1.408 +def get_buildid(work_dir, platform):
   1.409 +    """ extracts buildid from MAR
   1.410 +        TODO: this should handle 1.8 branch too
   1.411 +    """
   1.412 +    if platform == 'mac':
   1.413 +      ini = '%s/Contents/MacOS/application.ini' % work_dir
   1.414 +    else:
   1.415 +      ini = '%s/application.ini' % work_dir
   1.416 +    if not os.path.exists(ini):
   1.417 +        print 'WARNING: application.ini not found, cannot find build ID'
   1.418 +        return ''
   1.419 +    file = bz2.BZ2File(ini)
   1.420 +    for line in file:
   1.421 +      if line.find('BuildID') == 0:
   1.422 +        return line.strip().split('=')[1]
   1.423 +    print 'WARNING: cannot find build ID in application.ini'
   1.424 +    return ''
   1.425 +
   1.426 +def decode_filename(filepath):
   1.427 +    """ Breaks filename/dir structure into component parts based on regex
   1.428 +        for example: firefox-3.0b3pre.en-US.linux-i686.complete.mar
   1.429 +        Or linux-i686/en-US/firefox-3.0b3.complete.mar
   1.430 +        Returns dict with keys product, version, locale, platform, type
   1.431 +    """
   1.432 +    try:
   1.433 +      m = re.search(
   1.434 +        '(?P<product>\w+)(-)(?P<version>\w+\.\w+(\.\w+){0,2})(\.)(?P<locale>.+?)(\.)(?P<platform>.+?)(\.)(?P<type>\w+)(.mar)',
   1.435 +      os.path.basename(filepath))
   1.436 +      return m.groupdict()
   1.437 +    except Exception, exc:
   1.438 +      try:
   1.439 +        m = re.search(
   1.440 +          '(?P<platform>.+?)\/(?P<locale>.+?)\/(?P<product>\w+)-(?P<version>\w+\.\w+)\.(?P<type>\w+).mar',
   1.441 +        filepath)
   1.442 +        return m.groupdict()
   1.443 +      except:
   1.444 +        raise Exception("could not parse filepath %s: %s" % (filepath, exc))
   1.445 +
   1.446 +def create_partial_patches(patches):
   1.447 +    """ Given the patches generates a set of partial patches"""
   1.448 +    shas = {}
   1.449 +
   1.450 +    work_dir_root = None
   1.451 +    metadata = []
   1.452 +    try:
   1.453 +        work_dir_root = tempfile.mkdtemp('-fastmode', 'tmp', os.getcwd())
   1.454 +        print "Building patches using work dir: %s" % (work_dir_root)
   1.455 + 
   1.456 +        # Iterate through every patch set in the patch file
   1.457 +        patch_num = 1
   1.458 +        for patch in patches:
   1.459 +            startTime = time.time()
   1.460 +
   1.461 +            from_filename,to_filename,patch_filename,forced_updates = patch.split(",")
   1.462 +            from_filename,to_filename,patch_filename = os.path.abspath(from_filename),os.path.abspath(to_filename),os.path.abspath(patch_filename)
   1.463 +
   1.464 +            # Each patch iteration uses its own work dir
   1.465 +            work_dir = os.path.join(work_dir_root,str(patch_num))
   1.466 +            os.mkdir(work_dir)
   1.467 +
   1.468 +            # Extract from mar into from dir
   1.469 +            work_dir_from =  os.path.join(work_dir,"from");
   1.470 +            os.mkdir(work_dir_from)
   1.471 +            extract_mar(from_filename,work_dir_from)
   1.472 +            from_decoded = decode_filename(from_filename)
   1.473 +            from_buildid = get_buildid(work_dir_from, from_decoded['platform'])
   1.474 +            from_shasum = sha.sha(open(from_filename).read()).hexdigest()
   1.475 +            from_size = str(os.path.getsize(to_filename))
   1.476 +
   1.477 +            # Extract to mar into to dir
   1.478 +            work_dir_to =  os.path.join(work_dir,"to")
   1.479 +            os.mkdir(work_dir_to)
   1.480 +            extract_mar(to_filename, work_dir_to)
   1.481 +            to_decoded = decode_filename(from_filename)
   1.482 +            to_buildid = get_buildid(work_dir_to, to_decoded['platform'])
   1.483 +            to_shasum = sha.sha(open(to_filename).read()).hexdigest()
   1.484 +            to_size = str(os.path.getsize(to_filename))
   1.485 +
   1.486 +            mar_extract_time = time.time()
   1.487 +
   1.488 +            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'])
   1.489 +            partial_buildid = to_buildid
   1.490 +            partial_shasum = sha.sha(open(partial_filename).read()).hexdigest()
   1.491 +            partial_size = str(os.path.getsize(partial_filename))
   1.492 +
   1.493 +            metadata.append({
   1.494 +             'to_filename': os.path.basename(to_filename),
   1.495 +             'from_filename': os.path.basename(from_filename),
   1.496 +             'partial_filename': os.path.basename(partial_filename),
   1.497 +             'to_buildid':to_buildid, 
   1.498 +             'from_buildid':from_buildid, 
   1.499 +             'to_sha1sum':to_shasum, 
   1.500 +             'from_sha1sum':from_shasum, 
   1.501 +             'partial_sha1sum':partial_shasum, 
   1.502 +             'to_size':to_size,
   1.503 +             'from_size':from_size,
   1.504 +             'partial_size':partial_size,
   1.505 +             'to_version':to_decoded['version'],
   1.506 +             'from_version':from_decoded['version'],
   1.507 +             'locale':from_decoded['locale'],
   1.508 +             'platform':from_decoded['platform'],
   1.509 +            })
   1.510 +            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)
   1.511 +            patch_num += 1
   1.512 +        return metadata
   1.513 +    finally:
   1.514 +        # If we fail or get a ctrl-c during run be sure to clean up temp dir
   1.515 +        if (work_dir_root and os.path.exists(work_dir_root)):
   1.516 +            shutil.rmtree(work_dir_root)
   1.517 +
   1.518 +def main(argv):
   1.519 +    patchlist_file = None
   1.520 +    try:
   1.521 +         opts, args = getopt.getopt(argv, "hf:", ["help", "patchlist_file="])
   1.522 +         for opt, arg in opts:
   1.523 +            if opt in ("-h", "--help"):
   1.524 +                usage()
   1.525 +                sys.exit()
   1.526 +            elif opt in ("-f", "--patchlist_file"):
   1.527 +                patchlist_file = arg
   1.528 +    except getopt.GetoptError:
   1.529 +          usage()
   1.530 +          sys.exit(2)
   1.531 +
   1.532 +    if not patchlist_file:
   1.533 +        usage()
   1.534 +        sys.exit(2)
   1.535 +
   1.536 +    patches = []
   1.537 +    f = open(patchlist_file, 'r')
   1.538 +    for line in f.readlines():
   1.539 +        patches.append(line)
   1.540 +    f.close()
   1.541 +    create_partial_patches(patches)
   1.542 +
   1.543 +if __name__ == "__main__":
   1.544 +    main(sys.argv[1:])
   1.545 +

mercurial