5 from os
.path
import join
, getsize
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 manifest = set of patch instructions
22 files to exclude from this patch. names without slashes will be
23 excluded anywhere in the directory hiearchy. names with slashes
24 will only be excluded at that exact path
26 def __init__(self
, work_dir
, file_exclusion_list
, path_exclusion_list
):
27 self
.work_dir
=work_dir
30 self
.file_exclusion_list
=file_exclusion_list
31 self
.path_exclusion_list
=path_exclusion_list
33 def append_add_instruction(self
, filename
):
34 """ Appends an add instruction for this patch.
35 if the filename starts with extensions/ adds an add-if instruction
36 to test the existence of the subdirectory. This was ported from
37 mozilla/tools/update-packaging/common.sh/make_add_instruction
39 if filename
.startswith("extensions/"):
40 testdir
= "extensions/"+filename
.split("/")[1] # Dir immediately following extensions is used for the test
41 self
.manifest
.append('add-if "'+testdir
+'" "'+filename
+'"')
43 self
.manifest
.append('add "'+filename
+'"')
45 def append_patch_instruction(self
, filename
, patchname
):
46 """ Appends an patch instruction for this patch.
48 filename = file to patch
49 patchname = patchfile to apply to file
51 if the filename starts with extensions/ adds a patch-if instruction
52 to test the existence of the subdirectory.
53 if the filename starts with searchplugins/ add a add-if instruction for the filename
55 mozilla/tools/update-packaging/common.sh/make_patch_instruction
57 if filename
.startswith("extensions/"):
58 testdir
= "extensions/"+filename
.split("/")[1]
59 self
.manifest
.append('patch-if "'+testdir
+'" "'+patchname
+'" "'+filename
+'"')
60 elif filename
.startswith("searchplugins/"):
61 self
.manifest
.append('patch-if "'+filename
+'" "'+patchname
+'" "'+filename
+'"')
63 self
.manifest
.append('patch "'+patchname
+'" "'+filename
+'"')
65 def append_remove_instruction(self
, filename
):
66 """ Appends an remove instruction for this patch.
68 mozilla/tools/update-packaging/common.sh/make_remove_instruction
70 self
.manifest
.append('remove "'+filename
+'"')
72 def create_manifest_file(self
):
73 """ Createst the manifest file into to the root of the work_dir """
74 manifest_file_path
= os
.path
.join(self
.work_dir
,"update.manifest")
75 manifest_file
= open(manifest_file_path
, "w")
76 manifest_file
.writelines(string
.join(self
.manifest
, '\n'))
77 manifest_file
.writelines("\n")
80 bzip_file(manifest_file_path
)
81 self
.archive_files
.append('"update.manifest"')
84 def build_marfile_entry_hash(self
, root_path
):
85 """ Iterates through the root_path, creating a MarFileEntry for each file
86 in that path. Excluseds any filenames in the file_exclusion_list
90 for root
, dirs
, files
in os
.walk(root_path
):
92 # filename is relative path from root directory
93 partial_path
= root
[len(root_path
)+1:]
94 if name
not in self
.file_exclusion_list
:
95 filename
= os
.path
.join(partial_path
, name
)
96 if "/"+filename
not in self
.path_exclusion_list
:
97 mar_entry_hash
[filename
]=MarFileEntry(root_path
, filename
)
98 filename_set
.add(filename
)
99 return mar_entry_hash
, filename_set
103 """Represents a file inside a Mozilla Archive Format (MAR)
104 abs_path = abspath to the the file
105 name = relative path within the mar. e.g.
106 foo.mar/dir/bar.txt extracted into /tmp/foo:
107 abs_path=/tmp/foo/dir/bar.txt
110 def __init__(self
, root
, name
):
111 """root = path the the top of the mar
112 name = relative path within the mar"""
114 self
.abs_path
=os
.path
.join(root
,name
)
118 return 'Name: %s FullPath: %s' %(self
.name
,self
.abs_path
)
120 def calc_file_sha_digest(self
, filename
):
121 """ Returns sha digest of given filename"""
122 file_content
= open(filename
, 'r').read()
123 return sha
.new(file_content
).digest()
126 """ Returns sha digest of file repreesnted by this _marfile_entry\x10"""
127 if not self
.sha_cache
:
128 self
.sha_cache
=self
.calc_file_sha_digest(self
.abs_path
)
129 return self
.sha_cache
131 def exec_shell_cmd(cmd
):
132 """Execs shell cmd and raises an exception if the cmd fails"""
134 raise Exception, "cmd failed "+cmd
137 def copy_file(src_file_abs_path
, dst_file_abs_path
):
138 """ Copies src to dst creating any parent dirs required in dst first """
139 dst_file_dir
=os
.path
.dirname(dst_file_abs_path
)
140 if not os
.path
.exists(dst_file_dir
):
141 os
.makedirs(dst_file_dir
)
143 shutil
.copy2(src_file_abs_path
, dst_file_abs_path
)
145 def bzip_file(filename
):
146 """ Bzip's the file in place. The original file is replaced with a bzip'd version of itself
147 assumes the path is absolute"""
148 exec_shell_cmd('bzip2 -z9 "' + filename
+'"')
149 os
.rename(filename
+".bz2",filename
)
151 def bunzip_file(filename
):
152 """ Bzip's the file in palce. The original file is replaced with a bunzip'd version of itself.
153 doesn't matter if the filename ends in .bz2 or not"""
154 if not filename
.endswith(".bz2"):
155 os
.rename(filename
, filename
+".bz2")
156 filename
=filename
+".bz2"
157 exec_shell_cmd('bzip2 -d "' + filename
+'"')
160 def extract_mar(filename
, work_dir
):
161 """ Extracts the marfile intot he work_dir
162 assumes work_dir already exists otherwise will throw osError"""
163 print "Extracting "+filename
+" to "+work_dir
164 saved_path
= os
.getcwd()
167 exec_shell_cmd("mar -x "+filename
)
171 def create_partial_patch_for_file(from_marfile_entry
, to_marfile_entry
, shas
, patch_info
):
172 """ Creates the partial patch file and manifest entry for the pair of files passed in
174 if not (from_marfile_entry
.sha(),to_marfile_entry
.sha()) in shas
:
175 print "diffing: " + from_marfile_entry
.name
178 bunzip_file(from_marfile_entry
.abs_path
)
179 bunzip_file(to_marfile_entry
.abs_path
)
181 # The patch file will be created in the working directory with the
182 # name of the file in the mar + .patch
183 patch_file_abs_path
= os
.path
.join(patch_info
.work_dir
,from_marfile_entry
.name
+".patch")
184 patch_file_dir
=os
.path
.dirname(patch_file_abs_path
)
185 if not os
.path
.exists(patch_file_dir
):
186 os
.makedirs(patch_file_dir
)
188 # Create bzip'd patch file
189 exec_shell_cmd("mbsdiff "+from_marfile_entry
.abs_path
+" "+to_marfile_entry
.abs_path
+" "+patch_file_abs_path
)
190 bzip_file(patch_file_abs_path
)
192 # Create bzip's full file
193 full_file_abs_path
= os
.path
.join(patch_info
.work_dir
, to_marfile_entry
.name
)
194 shutil
.copy2(to_marfile_entry
.abs_path
, full_file_abs_path
)
195 bzip_file(full_file_abs_path
)
197 ## TOODO NEED TO ADD HANDLING FOR FORCED UPDATES
198 if os
.path
.getsize(patch_file_abs_path
) < os
.path
.getsize(full_file_abs_path
):
199 # Patch is smaller than file. Remove the file and add patch to manifest
200 os
.remove(full_file_abs_path
)
201 file_in_manifest_name
= from_marfile_entry
.name
+".patch"
202 file_in_manifest_abspath
= patch_file_abs_path
203 patch_info
.append_patch_instruction(to_marfile_entry
.name
, file_in_manifest_name
)
205 # File is smaller than patch. Remove the patch and add file to manifest
206 os
.remove(patch_file_abs_path
)
207 file_in_manifest_name
= from_marfile_entry
.name
208 file_in_manifest_abspath
= full_file_abs_path
209 patch_info
.append_add_instruction(file_in_manifest_name
)
211 shas
[from_marfile_entry
.sha(),to_marfile_entry
.sha()] = (file_in_manifest_name
,file_in_manifest_abspath
)
212 patch_info
.archive_files
.append('"'+file_in_manifest_name
+'"')
214 print "skipping diff: " + from_marfile_entry
.name
215 filename
, src_file_abs_path
= shas
[from_marfile_entry
.sha(),to_marfile_entry
.sha()]
216 # We've already calculated the patch for this pair of files.
217 if (filename
.endswith(".patch")):
218 # Patch was smaller than file - add patch instruction to manifest
219 file_in_manifest_name
= to_marfile_entry
.name
+'.patch';
220 patch_info
.append_patch_instruction(to_marfile_entry
.name
, file_in_manifest_name
)
222 # File was smaller than file - add file to manifest
223 file_in_manifest_name
= to_marfile_entry
.name
224 patch_info
.append_add_instruction(file_in_manifest_name
)
225 # Copy the pre-calculated file into our new patch work aread
226 copy_file(src_file_abs_path
, os
.path
.join(patch_info
.work_dir
, file_in_manifest_name
))
227 patch_info
.archive_files
.append('"'+file_in_manifest_name
+'"')
229 def create_add_patch_for_file(to_marfile_entry
, patch_info
):
230 """ Copy the file to the working dir, add the add instruction, and add it to the list of archive files """
231 print "Adding New File " + to_marfile_entry
.name
232 copy_file(to_marfile_entry
.abs_path
, os
.path
.join(patch_info
.work_dir
, to_marfile_entry
.name
))
233 patch_info
.append_add_instruction(to_marfile_entry
.name
)
234 patch_info
.archive_files
.append('"'+to_marfile_entry
.name
+'"')
236 def process_explicit_remove_files(dir_path
, patch_info
):
237 """ Looks for a 'removed-files' file in the dir_path. If the removed-files does not exist
238 this will throw. If found adds the removed-files
239 found in that file to the patch_info"""
241 # Windows and linux have this file at the root of the dir
242 list_file_path
= os
.path
.join(dir_path
, "removed-files")
244 if not os
.path
.exists(list_file_path
):
245 # Mac has is in Contents/MacOS/
246 prefix
= "Contents/MacOS"
247 list_file_path
= os
.path
.join(dir_path
, prefix
+"/removed-files")
249 if (os
.path
.exists(list_file_path
)):
250 list_file
= bz2
.BZ2File(list_file_path
,"r") # throws if doesn't exist
252 for line
in list_file
:
254 # Exclude any blank lines or any lines ending with a slash, which indicate
255 # directories. The updater doesn't know how to remove entire directories.
256 if line
and not line
.endswith("/"):
257 patch_info
.append_remove_instruction(os
.path
.join(prefix
,line
))
259 def create_partial_patch(from_dir_path
, to_dir_path
, patch_filename
, shas
, patch_info
):
260 """ Builds a partial patch by comparing the files in from_dir_path to thoes of to_dir_path"""
261 # Cannocolize the paths for safey
262 from_dir_path
= os
.path
.abspath(from_dir_path
)
263 to_dir_path
= os
.path
.abspath(to_dir_path
)
264 # First create a hashtable of the from and to directories
265 from_dir_hash
,from_dir_set
= patch_info
.build_marfile_entry_hash(from_dir_path
)
266 to_dir_hash
,to_dir_set
= patch_info
.build_marfile_entry_hash(to_dir_path
)
268 # Files which exist in both sets need to be patched
269 patch_filenames
= list(from_dir_set
.intersection(to_dir_set
))
270 patch_filenames
.sort()
271 for filename
in patch_filenames
:
272 from_marfile_entry
= from_dir_hash
[filename
]
273 to_marfile_entry
= to_dir_hash
[filename
]
274 if from_marfile_entry
.sha() != to_marfile_entry
.sha():
275 # Not the same - calculate a patch
276 create_partial_patch_for_file(from_marfile_entry
, to_marfile_entry
, shas
, patch_info
)
278 # files in from_dir not in to_dir need to be removed
279 remove_filenames
= list(from_dir_set
- to_dir_set
)
280 remove_filenames
.sort()
281 for filename
in remove_filenames
:
282 patch_info
.append_remove_instruction(from_dir_hash
[filename
].name
)
284 # files in to_dir not in from_dir need to added
285 add_filenames
= list(to_dir_set
- from_dir_set
)
287 for filename
in add_filenames
:
288 create_add_patch_for_file(to_dir_hash
[filename
], patch_info
)
290 process_explicit_remove_files(to_dir_path
, patch_info
)
292 # Construct Manifest file
293 patch_info
.create_manifest_file()
295 # And construct the mar
296 mar_cmd
= 'mar -C '+patch_info
.work_dir
+' -c output.mar '+string
.join(patch_info
.archive_files
, ' ')
297 exec_shell_cmd(mar_cmd
)
299 # Copy mar to final destination
300 patch_file_dir
= os
.path
.split(patch_filename
)[0]
301 if not os
.path
.exists(patch_file_dir
):
302 os
.makedirs(patch_file_dir
)
303 shutil
.copy2(os
.path
.join(patch_info
.work_dir
,"output.mar"), patch_filename
)
304 return patch_filename
308 print "-f for patchlist_file"
310 def get_buildid(work_dir
, platform
):
311 """ extracts buildid from MAR
312 TODO: this should handle 1.8 branch too
314 if platform
== 'mac':
315 ini
= '%s/Contents/MacOS/application.ini' % work_dir
317 ini
= '%s/application.ini' % work_dir
318 if not os
.path
.exists(ini
):
319 print 'WARNING: application.ini not found, cannot find build ID'
321 file = bz2
.BZ2File(ini
)
323 if line
.find('BuildID') == 0:
324 return line
.strip().split('=')[1]
325 print 'WARNING: cannot find build ID in application.ini'
328 def decode_filename(filepath
):
329 """ Breaks filename/dir structure into component parts based on regex
330 for example: firefox-3.0b3pre.en-US.linux-i686.complete.mar
331 Or linux-i686/en-US/firefox-3.0b3.complete.mar
332 Returns dict with keys product, version, locale, platform, type
336 '(?P<product>\w+)(-)(?P<version>\w+\.\w+)(\.)(?P<locale>.+?)(\.)(?P<platform>.+?)(\.)(?P<type>\w+)(.mar)',
337 os
.path
.basename(filepath
))
339 except Exception, exc
:
342 '(?P<platform>.+?)\/(?P<locale>.+?)\/(?P<product>\w+)-(?P<version>\w+\.\w+)\.(?P<type>\w+).mar',
346 raise Exception("could not parse filepath %s: %s" % (filepath
, exc
))
348 def create_partial_patches(patches
):
349 """ Given the patches generates a set of partial patches"""
355 work_dir_root
= tempfile
.mkdtemp('-fastmode', 'tmp', os
.getcwd())
356 print "Building patches using work dir: %s" % (work_dir_root
)
358 # Iterate through every patch set in the patch file
360 for patch
in patches
:
361 startTime
= time
.time()
363 from_filename
,to_filename
,patch_filename
,forced_updates
= patch
.split(",")
364 from_filename
,to_filename
,patch_filename
= os
.path
.abspath(from_filename
),os
.path
.abspath(to_filename
),os
.path
.abspath(patch_filename
)
366 # Each patch iteration uses its own work dir
367 work_dir
= os
.path
.join(work_dir_root
,str(patch_num
))
370 # Extract from mar into from dir
371 work_dir_from
= os
.path
.join(work_dir
,"from");
372 os
.mkdir(work_dir_from
)
373 extract_mar(from_filename
,work_dir_from
)
374 from_decoded
= decode_filename(from_filename
)
375 from_buildid
= get_buildid(work_dir_from
, from_decoded
['platform'])
376 from_shasum
= sha
.sha(open(from_filename
).read()).hexdigest()
377 from_size
= str(os
.path
.getsize(to_filename
))
379 # Extract to mar into to dir
380 work_dir_to
= os
.path
.join(work_dir
,"to")
381 os
.mkdir(work_dir_to
)
382 extract_mar(to_filename
, work_dir_to
)
383 to_decoded
= decode_filename(from_filename
)
384 to_buildid
= get_buildid(work_dir_to
, to_decoded
['platform'])
385 to_shasum
= sha
.sha(open(to_filename
).read()).hexdigest()
386 to_size
= str(os
.path
.getsize(to_filename
))
388 mar_extract_time
= time
.time()
390 partial_filename
= create_partial_patch(work_dir_from
, work_dir_to
, patch_filename
, shas
, PatchInfo(work_dir
, ['channel-prefs.js','update.manifest','removed-files'],['/readme.txt']))
391 partial_buildid
= to_buildid
392 partial_shasum
= sha
.sha(open(partial_filename
).read()).hexdigest()
393 partial_size
= str(os
.path
.getsize(partial_filename
))
396 'to_filename': os
.path
.basename(to_filename
),
397 'from_filename': os
.path
.basename(from_filename
),
398 'partial_filename': os
.path
.basename(partial_filename
),
399 'to_buildid':to_buildid
,
400 'from_buildid':from_buildid
,
401 'to_sha1sum':to_shasum
,
402 'from_sha1sum':from_shasum
,
403 'partial_sha1sum':partial_shasum
,
405 'from_size':from_size
,
406 'partial_size':partial_size
,
407 'to_version':to_decoded
['version'],
408 'from_version':from_decoded
['version'],
409 'locale':from_decoded
['locale'],
410 'platform':from_decoded
['platform'],
412 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
)
416 # If we fail or get a ctrl-c during run be sure to clean up temp dir
417 if (work_dir_root
and os
.path
.exists(work_dir_root
)):
418 shutil
.rmtree(work_dir_root
)
421 patchlist_file
= None
423 opts
, args
= getopt
.getopt(argv
, "hf:", ["help", "patchlist_file="])
424 for opt
, arg
in opts
:
425 if opt
in ("-h", "--help"):
428 elif opt
in ("-f", "--patchlist_file"):
430 except getopt
.GetoptError
:
434 if not patchlist_file
:
439 f
= open(patchlist_file
, 'r')
440 for line
in f
.readlines():
443 create_partial_patches(patches
)
445 if __name__
== "__main__":