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/.
17 """ Represents the meta-data associated with a patch
18 work_dir = working dir where files are stored for this patch
19 archive_files = list of files to include in this patch
20 manifestv2 = set of manifest version 2 patch instructions
21 manifestv3 = set of manifest version 3 patch instructions
23 files to exclude from this patch. names without slashes will be
24 excluded anywhere in the directory hiearchy. names with slashes
25 will only be excluded at that exact path
28 def __init__(self
, work_dir
, file_exclusion_list
, path_exclusion_list
):
29 self
.work_dir
= work_dir
30 self
.archive_files
= []
33 self
.file_exclusion_list
= file_exclusion_list
34 self
.path_exclusion_list
= path_exclusion_list
36 def append_add_instruction(self
, filename
):
37 """ Appends an add instruction for this patch.
38 if filename starts with distribution/extensions/.*/ this will add an
39 add-if instruction that will add the file if the parent directory
40 of the file exists. This was ported from
41 mozilla/tools/update-packaging/common.sh's make_add_instruction.
43 m
= re
.match("((?:|.*/)distribution/extensions/.*)/", filename
)
45 # Directory immediately following extensions is used for the test
47 print(' add-if "' + testdir
+ '" "' + filename
+ '"')
48 self
.manifestv2
.append('add-if "' + testdir
+ '" "' + filename
+ '"')
49 self
.manifestv3
.append('add-if "' + testdir
+ '" "' + filename
+ '"')
51 print(' add "' + filename
+ '"')
52 self
.manifestv2
.append('add "' + filename
+ '"')
53 self
.manifestv3
.append('add "' + filename
+ '"')
55 def append_add_if_not_instruction(self
, filename
):
56 """ Appends an add-if-not instruction to the version 3 manifest for this patch.
57 This was ported from mozilla/tools/update-packaging/common.sh's
58 make_add_if_not_instruction.
60 print(' add-if-not "' + filename
+ '" "' + filename
+ '"')
61 self
.manifestv3
.append('add-if-not "' + filename
+ '" "' + filename
+ '"')
63 def append_patch_instruction(self
, filename
, patchname
):
64 """ Appends a patch instruction for this patch.
66 filename = file to patch
67 patchname = patchfile to apply to file
69 if filename starts with distribution/extensions/.*/ this will add a
70 patch-if instruction that will patch the file if the parent
71 directory of the file exists. This was ported from
72 mozilla/tools/update-packaging/common.sh's make_patch_instruction.
74 m
= re
.match("((?:|.*/)distribution/extensions/.*)/", filename
)
77 print(' patch-if "' + testdir
+ '" "' + patchname
+ '" "' + filename
+ '"')
78 self
.manifestv2
.append('patch-if "' + testdir
+ '" "' +
79 patchname
+ '" "' + filename
+ '"')
80 self
.manifestv3
.append('patch-if "' + testdir
+ '" "' +
81 patchname
+ '" "' + filename
+ '"')
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.
90 mozilla/tools/update-packaging/common.sh/make_remove_instruction
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
+ '"')
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(io
.BytesIO(b
"type \"partial\"\n"))
111 manifest_file
.writelines(io
.BytesIO('\n'.join(self
.manifestv2
).encode('ascii')))
112 manifest_file
.writelines(io
.BytesIO(b
"\n"))
113 manifest_file
.close()
115 xz_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(io
.BytesIO(b
"type \"partial\"\n"))
122 manifest_file
.writelines(io
.BytesIO('\n'.join(self
.manifestv3
).encode('ascii')))
123 manifest_file
.writelines(io
.BytesIO(b
"\n"))
124 manifest_file
.close()
126 xz_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
136 for root
, dirs
, files
in os
.walk(root_path
):
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
)
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
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
168 def __init__(self
, root
, name
):
169 """root = path the the top of the mar
170 name = relative path within the mar"""
171 self
.name
= name
.replace("\\", "/")
172 self
.abs_path
= os
.path
.join(root
, name
)
173 self
.sha_cache
= None
176 return 'Name: %s FullPath: %s' % (self
.name
, self
.abs_path
)
178 def calc_file_sha_digest(self
, filename
):
179 """ Returns sha digest of given filename"""
180 file_content
= open(filename
, 'rb').read()
181 return hashlib
.sha1(file_content
).digest()
184 """ Returns sha digest of file repreesnted by this _marfile_entry\x10"""
185 if not self
.sha_cache
:
186 self
.sha_cache
= self
.calc_file_sha_digest(self
.abs_path
)
187 return self
.sha_cache
190 def exec_shell_cmd(cmd
):
191 """Execs shell cmd and raises an exception if the cmd fails"""
193 raise Exception("cmd failed " + cmd
)
196 def copy_file(src_file_abs_path
, dst_file_abs_path
):
197 """ Copies src to dst creating any parent dirs required in dst first """
198 dst_file_dir
= os
.path
.dirname(dst_file_abs_path
)
199 if not os
.path
.exists(dst_file_dir
):
200 os
.makedirs(dst_file_dir
)
202 shutil
.copy2(src_file_abs_path
, dst_file_abs_path
)
205 def xz_file(filename
):
206 """ XZ compresses the file in place. The original file is replaced
207 with the xz compressed version of itself assumes the path is absolute"""
208 exec_shell_cmd('xz --compress --x86 --lzma2 --format=xz --check=crc64 "' + filename
+ '"')
209 os
.rename(filename
+ ".xz", filename
)
212 def xzunzip_file(filename
):
213 """ xz decompresses the file in palce. The original file is replaced
214 with a xz decompressed version of itself. doesn't matter if the
215 filename ends in .xz or not"""
216 if not filename
.endswith(".xz"):
217 os
.rename(filename
, filename
+ ".xz")
218 filename
= filename
+ ".xz"
219 exec_shell_cmd('xz -d "' + filename
+ '"')
222 def extract_mar(filename
, work_dir
):
223 """ Extracts the marfile intot he work_dir
224 assumes work_dir already exists otherwise will throw osError"""
225 print("Extracting " + filename
+ " to " + work_dir
)
226 saved_path
= os
.getcwd()
229 exec_shell_cmd("mar -x " + filename
)
234 def create_partial_patch_for_file(from_marfile_entry
, to_marfile_entry
, shas
, patch_info
):
235 """ Creates the partial patch file and manifest entry for the pair of files passed in
237 if not (from_marfile_entry
.sha(), to_marfile_entry
.sha()) in shas
:
238 print('diffing "' + from_marfile_entry
.name
+ '\"')
240 xzunzip_file(from_marfile_entry
.abs_path
)
241 xzunzip_file(to_marfile_entry
.abs_path
)
243 # The patch file will be created in the working directory with the
244 # name of the file in the mar + .patch
245 patch_file_abs_path
= os
.path
.join(patch_info
.work_dir
, from_marfile_entry
.name
+ ".patch")
246 patch_file_dir
= os
.path
.dirname(patch_file_abs_path
)
247 if not os
.path
.exists(patch_file_dir
):
248 os
.makedirs(patch_file_dir
)
250 # Create xz compressed patch file
251 exec_shell_cmd("mbsdiff " + from_marfile_entry
.abs_path
+ " " +
252 to_marfile_entry
.abs_path
+ " " + patch_file_abs_path
)
253 xz_file(patch_file_abs_path
)
255 # Create xz compressed full file
256 full_file_abs_path
= os
.path
.join(patch_info
.work_dir
, to_marfile_entry
.name
)
257 shutil
.copy2(to_marfile_entry
.abs_path
, full_file_abs_path
)
258 xz_file(full_file_abs_path
)
260 if os
.path
.getsize(patch_file_abs_path
) < os
.path
.getsize(full_file_abs_path
):
261 # Patch is smaller than file. Remove the file and add patch to manifest
262 os
.remove(full_file_abs_path
)
263 file_in_manifest_name
= from_marfile_entry
.name
+ ".patch"
264 file_in_manifest_abspath
= patch_file_abs_path
265 patch_info
.append_patch_instruction(to_marfile_entry
.name
, file_in_manifest_name
)
267 # File is smaller than patch. Remove the patch and add file to manifest
268 os
.remove(patch_file_abs_path
)
269 file_in_manifest_name
= from_marfile_entry
.name
270 file_in_manifest_abspath
= full_file_abs_path
271 patch_info
.append_add_instruction(file_in_manifest_name
)
273 shas
[from_marfile_entry
.sha(), to_marfile_entry
.sha()] = (
274 file_in_manifest_name
, file_in_manifest_abspath
)
275 patch_info
.archive_files
.append('"' + file_in_manifest_name
+ '"')
277 filename
, src_file_abs_path
= shas
[from_marfile_entry
.sha(), to_marfile_entry
.sha()]
278 # We've already calculated the patch for this pair of files.
279 if (filename
.endswith(".patch")):
280 # print "skipping diff: "+from_marfile_entry.name
281 # Patch was smaller than file - add patch instruction to manifest
282 file_in_manifest_name
= to_marfile_entry
.name
+ '.patch'
283 patch_info
.append_patch_instruction(to_marfile_entry
.name
, file_in_manifest_name
)
285 # File was smaller than file - add file to manifest
286 file_in_manifest_name
= to_marfile_entry
.name
287 patch_info
.append_add_instruction(file_in_manifest_name
)
288 # Copy the pre-calculated file into our new patch work aread
289 copy_file(src_file_abs_path
, os
.path
.join(patch_info
.work_dir
, file_in_manifest_name
))
290 patch_info
.archive_files
.append('"' + file_in_manifest_name
+ '"')
293 def create_add_patch_for_file(to_marfile_entry
, patch_info
):
294 """ Copy the file to the working dir, add the add instruction,
295 and add it to the list of archive files """
296 copy_file(to_marfile_entry
.abs_path
, os
.path
.join(patch_info
.work_dir
, to_marfile_entry
.name
))
297 patch_info
.append_add_instruction(to_marfile_entry
.name
)
298 patch_info
.archive_files
.append('"' + to_marfile_entry
.name
+ '"')
301 def create_add_if_not_patch_for_file(to_marfile_entry
, patch_info
):
302 """ Copy the file to the working dir, add the add-if-not instruction,
303 and add it to the list of archive files """
304 copy_file(to_marfile_entry
.abs_path
, os
.path
.join(patch_info
.work_dir
, to_marfile_entry
.name
))
305 patch_info
.append_add_if_not_instruction(to_marfile_entry
.name
)
306 patch_info
.archive_files
.append('"' + to_marfile_entry
.name
+ '"')
309 def process_explicit_remove_files(dir_path
, patch_info
):
310 """ Looks for a 'removed-files' file in the dir_path. If the removed-files does not exist
311 this will throw. If found adds the removed-files
312 found in that file to the patch_info"""
314 # Windows and linux have this file at the root of the dir
315 list_file_path
= os
.path
.join(dir_path
, "removed-files")
316 if not os
.path
.exists(list_file_path
):
317 list_file_path
= os
.path
.join(dir_path
, "Contents/Resources/removed-files")
319 if (os
.path
.exists(list_file_path
)):
320 fd
, tmppath
= tempfile
.mkstemp('', 'tmp', os
.getcwd())
322 exec_shell_cmd('xz -k -d --stdout "' + list_file_path
+ '" > "' + tmppath
+ '"')
323 list_file
= open(tmppath
)
326 for line
in list_file
:
327 lines
.append(line
.strip())
331 lines
.sort(reverse
=True)
333 # Exclude any blank and comment lines.
334 if line
and not line
.startswith("#"):
335 # Python on windows uses \ for path separators and the update
336 # manifests expects / for path separators on all platforms.
337 line
= line
.replace("\\", "/")
338 patch_info
.append_remove_instruction(line
)
341 def create_partial_patch(from_dir_path
, to_dir_path
, patch_filename
,
342 shas
, patch_info
, forced_updates
, add_if_not_list
):
343 """ Builds a partial patch by comparing the files in from_dir_path to those of to_dir_path"""
344 # Cannocolize the paths for safey
345 from_dir_path
= os
.path
.abspath(from_dir_path
)
346 to_dir_path
= os
.path
.abspath(to_dir_path
)
347 # Create a hashtable of the from and to directories
348 from_dir_hash
, from_file_set
, from_dir_set
= patch_info
.build_marfile_entry_hash(from_dir_path
)
349 to_dir_hash
, to_file_set
, to_dir_set
= patch_info
.build_marfile_entry_hash(to_dir_path
)
350 # Create a list of the forced updates
351 forced_list
= forced_updates
.strip().split('|')
352 # Require that the precomplete file is included in the complete update
353 if "precomplete" in to_file_set
:
354 forced_list
.append("precomplete")
355 elif "Contents/Resources/precomplete" in to_file_set
:
356 forced_list
.append("Contents/Resources/precomplete")
357 # The check with \ file separators allows tests for Mac to run on Windows
358 elif "Contents\Resources\precomplete" in to_file_set
:
359 forced_list
.append("Contents\Resources\precomplete")
361 raise Exception("missing precomplete file in: " + to_dir_path
)
363 if "removed-files" in to_file_set
:
364 forced_list
.append("removed-files")
365 elif "Contents/Resources/removed-files" in to_file_set
:
366 forced_list
.append("Contents/Resources/removed-files")
367 # The check with \ file separators allows tests for Mac to run on Windows
368 elif "Contents\Resources\\removed-files" in to_file_set
:
369 forced_list
.append("Contents\Resources\\removed-files")
371 raise Exception("missing removed-files file in: " + to_dir_path
)
373 # Files which exist in both sets need to be patched
374 patch_filenames
= list(from_file_set
.intersection(to_file_set
))
375 patch_filenames
.sort(reverse
=True)
376 for filename
in patch_filenames
:
377 from_marfile_entry
= from_dir_hash
[filename
]
378 to_marfile_entry
= to_dir_hash
[filename
]
379 if os
.path
.basename(filename
) in add_if_not_list
:
380 # This filename is in the add if not list, explicitly add-if-not
381 create_add_if_not_patch_for_file(to_dir_hash
[filename
], patch_info
)
382 elif filename
in forced_list
:
383 print('Forcing "' + filename
+ '"')
384 # This filename is in the forced list, explicitly add
385 create_add_patch_for_file(to_dir_hash
[filename
], patch_info
)
387 if from_marfile_entry
.sha() != to_marfile_entry
.sha():
388 # Not the same - calculate a patch
389 create_partial_patch_for_file(
390 from_marfile_entry
, to_marfile_entry
, shas
, patch_info
)
392 # files in to_dir not in from_dir need to added
393 add_filenames
= list(to_file_set
- from_file_set
)
394 add_filenames
.sort(reverse
=True)
395 for filename
in add_filenames
:
396 if os
.path
.basename(filename
) in add_if_not_list
:
397 create_add_if_not_patch_for_file(to_dir_hash
[filename
], patch_info
)
399 create_add_patch_for_file(to_dir_hash
[filename
], patch_info
)
401 # files in from_dir not in to_dir need to be removed
402 remove_filenames
= list(from_file_set
- to_file_set
)
403 remove_filenames
.sort(reverse
=True)
404 for filename
in remove_filenames
:
405 patch_info
.append_remove_instruction(from_dir_hash
[filename
].name
)
407 process_explicit_remove_files(to_dir_path
, patch_info
)
409 # directories in from_dir not in to_dir need to be removed
410 remove_dirnames
= list(from_dir_set
- to_dir_set
)
411 remove_dirnames
.sort(reverse
=True)
412 for dirname
in remove_dirnames
:
413 patch_info
.append_remove_instruction(from_dir_hash
[dirname
].name
)
415 # Construct the Manifest files
416 patch_info
.create_manifest_files()
418 # And construct the mar
419 mar_cmd
= 'mar -C ' + patch_info
.work_dir
+ \
420 ' -c output.mar ' + ' '.join(patch_info
.archive_files
)
421 exec_shell_cmd(mar_cmd
)
423 # Copy mar to final destination
424 patch_file_dir
= os
.path
.split(patch_filename
)[0]
425 if not os
.path
.exists(patch_file_dir
):
426 os
.makedirs(patch_file_dir
)
427 shutil
.copy2(os
.path
.join(patch_info
.work_dir
, "output.mar"), patch_filename
)
429 return patch_filename
434 print("-f for patchlist_file")
437 def get_buildid(work_dir
):
438 """ extracts buildid from MAR
440 ini
= '%s/application.ini' % work_dir
441 if not os
.path
.exists(ini
):
442 ini
= '%s/Contents/Resources/application.ini' % work_dir
443 if not os
.path
.exists(ini
):
444 print('WARNING: application.ini not found, cannot find build ID')
447 fd
, tmppath
= tempfile
.mkstemp('', 'tmp', os
.getcwd())
449 exec_shell_cmd('xz -k -d --stdout "' + ini
+ '" > "' + tmppath
+ '"')
452 if line
.find('BuildID') == 0:
455 return line
.strip().split('=')[1]
456 print('WARNING: cannot find build ID in application.ini')
462 def decode_filename(filepath
):
463 """ Breaks filename/dir structure into component parts based on regex
464 for example: firefox-3.0b3pre.en-US.linux-i686.complete.mar
465 Or linux-i686/en-US/firefox-3.0b3.complete.mar
466 Returns dict with keys product, version, locale, platform, type
470 '(?P<product>\w+)(-)(?P<version>\w+\.\w+(\.\w+){0,2})(\.)(?P<locale>.+?)(\.)(?P<platform>.+?)(\.)(?P<type>\w+)(.mar)', # NOQA: E501
471 os
.path
.basename(filepath
))
473 except Exception as exc
:
476 '(?P<platform>.+?)\/(?P<locale>.+?)\/(?P<product>\w+)-(?P<version>\w+\.\w+)\.(?P<type>\w+).mar', # NOQA: E501
480 raise Exception("could not parse filepath %s: %s" % (filepath
, exc
))
483 def create_partial_patches(patches
):
484 """ Given the patches generates a set of partial patches"""
490 work_dir_root
= tempfile
.mkdtemp('-fastmode', 'tmp', os
.getcwd())
491 print("Building patches using work dir: %s" % (work_dir_root
))
493 # Iterate through every patch set in the patch file
495 for patch
in patches
:
496 startTime
= time
.time()
498 from_filename
, to_filename
, patch_filename
, forced_updates
= patch
.split(",")
499 from_filename
, to_filename
, patch_filename
= os
.path
.abspath(
500 from_filename
), os
.path
.abspath(to_filename
), os
.path
.abspath(patch_filename
)
502 # Each patch iteration uses its own work dir
503 work_dir
= os
.path
.join(work_dir_root
, str(patch_num
))
506 # Extract from mar into from dir
507 work_dir_from
= os
.path
.join(work_dir
, "from")
508 os
.mkdir(work_dir_from
)
509 extract_mar(from_filename
, work_dir_from
)
510 from_decoded
= decode_filename(from_filename
)
511 from_buildid
= get_buildid(work_dir_from
)
512 from_shasum
= hashlib
.sha1(open(from_filename
, "rb").read()).hexdigest()
513 from_size
= str(os
.path
.getsize(to_filename
))
515 # Extract to mar into to dir
516 work_dir_to
= os
.path
.join(work_dir
, "to")
517 os
.mkdir(work_dir_to
)
518 extract_mar(to_filename
, work_dir_to
)
519 to_decoded
= decode_filename(from_filename
)
520 to_buildid
= get_buildid(work_dir_to
)
521 to_shasum
= hashlib
.sha1(open(to_filename
, 'rb').read()).hexdigest()
522 to_size
= str(os
.path
.getsize(to_filename
))
524 mar_extract_time
= time
.time()
526 partial_filename
= create_partial_patch(work_dir_from
, work_dir_to
, patch_filename
,
527 shas
, PatchInfo(work_dir
, [
533 ['channel-prefs.js', 'update-settings.ini'])
534 partial_shasum
= hashlib
.sha1(open(partial_filename
, "rb").read()).hexdigest()
535 partial_size
= str(os
.path
.getsize(partial_filename
))
538 'to_filename': os
.path
.basename(to_filename
),
539 'from_filename': os
.path
.basename(from_filename
),
540 'partial_filename': os
.path
.basename(partial_filename
),
541 'to_buildid': to_buildid
,
542 'from_buildid': from_buildid
,
543 'to_sha1sum': to_shasum
,
544 'from_sha1sum': from_shasum
,
545 'partial_sha1sum': partial_shasum
,
547 'from_size': from_size
,
548 'partial_size': partial_size
,
549 'to_version': to_decoded
['version'],
550 'from_version': from_decoded
['version'],
551 'locale': from_decoded
['locale'],
552 'platform': from_decoded
['platform'],
554 print("done with patch %s/%s time (%.2fs/%.2fs/%.2fs) (mar/patch/total)" % (str(patch_num
), # NOQA: E501
555 str(len(patches
)), mar_extract_time
- startTime
, time
.time() - mar_extract_time
, time
.time() - startTime
)) # NOQA: E501
559 # If we fail or get a ctrl-c during run be sure to clean up temp dir
560 if (work_dir_root
and os
.path
.exists(work_dir_root
)):
561 shutil
.rmtree(work_dir_root
)
565 patchlist_file
= None
567 opts
, args
= getopt
.getopt(argv
, "hf:", ["help", "patchlist_file="])
568 for opt
, arg
in opts
:
569 if opt
in ("-h", "--help"):
572 elif opt
in ("-f", "--patchlist_file"):
574 except getopt
.GetoptError
:
578 if not patchlist_file
:
583 f
= open(patchlist_file
, 'r')
584 for line
in f
.readlines():
587 create_partial_patches(patches
)
590 if __name__
== "__main__":