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