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