blob: 07ed1556bdc969440a14ffd11e3adaad560cd5b1 [file] [log] [blame]
Doug Zongkereef39442009-04-02 12:14:19 -07001#!/usr/bin/env python
2#
3# Copyright (C) 2008 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Given a target-files zipfile, produces an OTA package that installs
19that build. An incremental OTA is produced if -i is given, otherwise
20a full OTA is produced.
21
22Usage: ota_from_target_files [flags] input_target_files output_ota_package
23
24 -b (--board_config) <file>
25 Specifies a BoardConfig.mk file containing image max sizes
26 against which the generated image files are checked.
27
28 -k (--package_key) <key>
29 Key to use to sign the package (default is
30 "build/target/product/security/testkey").
31
32 -i (--incremental_from) <file>
33 Generate an incremental OTA using the given target-files zip as
34 the starting build.
35
Doug Zongkerdbfaae52009-04-21 17:12:54 -070036 -w (--wipe_user_data)
37 Generate an OTA package that will wipe the user data partition
38 when installed.
39
Doug Zongkereef39442009-04-02 12:14:19 -070040"""
41
42import sys
43
44if sys.hexversion < 0x02040000:
45 print >> sys.stderr, "Python 2.4 or newer is required."
46 sys.exit(1)
47
48import copy
49import os
50import re
51import sha
52import subprocess
53import tempfile
54import time
55import zipfile
56
57import common
58
59OPTIONS = common.OPTIONS
60OPTIONS.package_key = "build/target/product/security/testkey"
61OPTIONS.incremental_source = None
62OPTIONS.require_verbatim = set()
63OPTIONS.prohibit_verbatim = set(("system/build.prop",))
64OPTIONS.patch_threshold = 0.95
Doug Zongkerdbfaae52009-04-21 17:12:54 -070065OPTIONS.wipe_user_data = False
Doug Zongkereef39442009-04-02 12:14:19 -070066
67def MostPopularKey(d, default):
68 """Given a dict, return the key corresponding to the largest
69 value. Returns 'default' if the dict is empty."""
70 x = [(v, k) for (k, v) in d.iteritems()]
71 if not x: return default
72 x.sort()
73 return x[-1][1]
74
75
76def IsSymlink(info):
77 """Return true if the zipfile.ZipInfo object passed in represents a
78 symlink."""
79 return (info.external_attr >> 16) == 0120777
80
81
82
83class Item:
84 """Items represent the metadata (user, group, mode) of files and
85 directories in the system image."""
86 ITEMS = {}
87 def __init__(self, name, dir=False):
88 self.name = name
89 self.uid = None
90 self.gid = None
91 self.mode = None
92 self.dir = dir
93
94 if name:
95 self.parent = Item.Get(os.path.dirname(name), dir=True)
96 self.parent.children.append(self)
97 else:
98 self.parent = None
99 if dir:
100 self.children = []
101
102 def Dump(self, indent=0):
103 if self.uid is not None:
104 print "%s%s %d %d %o" % (" "*indent, self.name, self.uid, self.gid, self.mode)
105 else:
106 print "%s%s %s %s %s" % (" "*indent, self.name, self.uid, self.gid, self.mode)
107 if self.dir:
108 print "%s%s" % (" "*indent, self.descendants)
109 print "%s%s" % (" "*indent, self.best_subtree)
110 for i in self.children:
111 i.Dump(indent=indent+1)
112
113 @classmethod
114 def Get(cls, name, dir=False):
115 if name not in cls.ITEMS:
116 cls.ITEMS[name] = Item(name, dir=dir)
117 return cls.ITEMS[name]
118
119 @classmethod
120 def GetMetadata(cls):
121 """Run the external 'fs_config' program to determine the desired
122 uid, gid, and mode for every Item object."""
123 p = common.Run(["fs_config"], stdin=subprocess.PIPE,
124 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
125 suffix = { False: "", True: "/" }
126 input = "".join(["%s%s\n" % (i.name, suffix[i.dir])
127 for i in cls.ITEMS.itervalues() if i.name])
128 output, error = p.communicate(input)
129 assert not error
130
131 for line in output.split("\n"):
132 if not line: continue
133 name, uid, gid, mode = line.split()
134 i = cls.ITEMS[name]
135 i.uid = int(uid)
136 i.gid = int(gid)
137 i.mode = int(mode, 8)
138 if i.dir:
139 i.children.sort(key=lambda i: i.name)
140
141 def CountChildMetadata(self):
142 """Count up the (uid, gid, mode) tuples for all children and
143 determine the best strategy for using set_perm_recursive and
144 set_perm to correctly chown/chmod all the files to their desired
145 values. Recursively calls itself for all descendants.
146
147 Returns a dict of {(uid, gid, dmode, fmode): count} counting up
148 all descendants of this node. (dmode or fmode may be None.) Also
149 sets the best_subtree of each directory Item to the (uid, gid,
150 dmode, fmode) tuple that will match the most descendants of that
151 Item.
152 """
153
154 assert self.dir
155 d = self.descendants = {(self.uid, self.gid, self.mode, None): 1}
156 for i in self.children:
157 if i.dir:
158 for k, v in i.CountChildMetadata().iteritems():
159 d[k] = d.get(k, 0) + v
160 else:
161 k = (i.uid, i.gid, None, i.mode)
162 d[k] = d.get(k, 0) + 1
163
164 # Find the (uid, gid, dmode, fmode) tuple that matches the most
165 # descendants.
166
167 # First, find the (uid, gid) pair that matches the most
168 # descendants.
169 ug = {}
170 for (uid, gid, _, _), count in d.iteritems():
171 ug[(uid, gid)] = ug.get((uid, gid), 0) + count
172 ug = MostPopularKey(ug, (0, 0))
173
174 # Now find the dmode and fmode that match the most descendants
175 # with that (uid, gid), and choose those.
176 best_dmode = (0, 0755)
177 best_fmode = (0, 0644)
178 for k, count in d.iteritems():
179 if k[:2] != ug: continue
180 if k[2] is not None and count >= best_dmode[0]: best_dmode = (count, k[2])
181 if k[3] is not None and count >= best_fmode[0]: best_fmode = (count, k[3])
182 self.best_subtree = ug + (best_dmode[1], best_fmode[1])
183
184 return d
185
186 def SetPermissions(self, script, renamer=lambda x: x):
187 """Append set_perm/set_perm_recursive commands to 'script' to
188 set all permissions, users, and groups for the tree of files
189 rooted at 'self'. 'renamer' turns the filenames stored in the
190 tree of Items into the strings used in the script."""
191
192 self.CountChildMetadata()
193
194 def recurse(item, current):
195 # current is the (uid, gid, dmode, fmode) tuple that the current
196 # item (and all its children) have already been set to. We only
197 # need to issue set_perm/set_perm_recursive commands if we're
198 # supposed to be something different.
199 if item.dir:
200 if current != item.best_subtree:
201 script.append("set_perm_recursive %d %d 0%o 0%o %s" %
202 (item.best_subtree + (renamer(item.name),)))
203 current = item.best_subtree
204
205 if item.uid != current[0] or item.gid != current[1] or \
206 item.mode != current[2]:
207 script.append("set_perm %d %d 0%o %s" %
208 (item.uid, item.gid, item.mode, renamer(item.name)))
209
210 for i in item.children:
211 recurse(i, current)
212 else:
213 if item.uid != current[0] or item.gid != current[1] or \
214 item.mode != current[3]:
215 script.append("set_perm %d %d 0%o %s" %
216 (item.uid, item.gid, item.mode, renamer(item.name)))
217
218 recurse(self, (-1, -1, -1, -1))
219
220
221def CopySystemFiles(input_zip, output_zip=None,
222 substitute=None):
223 """Copies files underneath system/ in the input zip to the output
224 zip. Populates the Item class with their metadata, and returns a
225 list of symlinks. output_zip may be None, in which case the copy is
226 skipped (but the other side effects still happen). substitute is an
227 optional dict of {output filename: contents} to be output instead of
228 certain input files.
229 """
230
231 symlinks = []
232
233 for info in input_zip.infolist():
234 if info.filename.startswith("SYSTEM/"):
235 basefilename = info.filename[7:]
236 if IsSymlink(info):
237 symlinks.append((input_zip.read(info.filename),
238 "SYSTEM:" + basefilename))
239 else:
240 info2 = copy.copy(info)
241 fn = info2.filename = "system/" + basefilename
242 if substitute and fn in substitute and substitute[fn] is None:
243 continue
244 if output_zip is not None:
245 if substitute and fn in substitute:
246 data = substitute[fn]
247 else:
248 data = input_zip.read(info.filename)
249 output_zip.writestr(info2, data)
250 if fn.endswith("/"):
251 Item.Get(fn[:-1], dir=True)
252 else:
253 Item.Get(fn, dir=False)
254
255 symlinks.sort()
256 return symlinks
257
258
259def AddScript(script, output_zip):
260 now = time.localtime()
261 i = zipfile.ZipInfo("META-INF/com/google/android/update-script",
262 (now.tm_year, now.tm_mon, now.tm_mday,
263 now.tm_hour, now.tm_min, now.tm_sec))
264 output_zip.writestr(i, "\n".join(script) + "\n")
265
266
267def SignOutput(temp_zip_name, output_zip_name):
268 key_passwords = common.GetKeyPasswords([OPTIONS.package_key])
269 pw = key_passwords[OPTIONS.package_key]
270
271 common.SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw)
272
273
274def SubstituteRoot(s):
275 if s == "system": return "SYSTEM:"
276 assert s.startswith("system/")
277 return "SYSTEM:" + s[7:]
278
279def FixPermissions(script):
280 Item.GetMetadata()
281 root = Item.Get("system")
282 root.SetPermissions(script, renamer=SubstituteRoot)
283
284def DeleteFiles(script, to_delete):
285 line = []
286 t = 0
287 for i in to_delete:
288 line.append(i)
289 t += len(i) + 1
290 if t > 80:
291 script.append("delete " + " ".join(line))
292 line = []
293 t = 0
294 if line:
295 script.append("delete " + " ".join(line))
296
297def AppendAssertions(script, input_zip):
298 script.append('assert compatible_with("0.2") == "true"')
299
300 device = GetBuildProp("ro.product.device", input_zip)
301 script.append('assert getprop("ro.product.device") == "%s" || '
302 'getprop("ro.build.product") == "%s"' % (device, device))
303
304 info = input_zip.read("OTA/android-info.txt")
305 m = re.search(r"require\s+version-bootloader\s*=\s*(\S+)", info)
306 if not m:
307 raise ExternalError("failed to find required bootloaders in "
308 "android-info.txt")
309 bootloaders = m.group(1).split("|")
310 script.append("assert " +
311 " || ".join(['getprop("ro.bootloader") == "%s"' % (b,)
312 for b in bootloaders]))
313
314
315def IncludeBinary(name, input_zip, output_zip):
316 try:
317 data = input_zip.read(os.path.join("OTA/bin", name))
318 output_zip.writestr(name, data)
319 except IOError:
320 raise ExternalError('unable to include device binary "%s"' % (name,))
321
322
323def WriteFullOTAPackage(input_zip, output_zip):
324 script = []
325
326 ts = GetBuildProp("ro.build.date.utc", input_zip)
327 script.append("run_program PACKAGE:check_prereq %s" % (ts,))
328 IncludeBinary("check_prereq", input_zip, output_zip)
329
330 AppendAssertions(script, input_zip)
331
332 script.append("format BOOT:")
333 script.append("show_progress 0.1 0")
334
335 output_zip.writestr("radio.img", input_zip.read("RADIO/image"))
336 script.append("write_radio_image PACKAGE:radio.img")
337 script.append("show_progress 0.5 0")
338
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700339 if OPTIONS.wipe_user_data:
340 script.append("format DATA:")
341
Doug Zongkereef39442009-04-02 12:14:19 -0700342 script.append("format SYSTEM:")
343 script.append("copy_dir PACKAGE:system SYSTEM:")
344
345 symlinks = CopySystemFiles(input_zip, output_zip)
346 script.extend(["symlink %s %s" % s for s in symlinks])
347
348 common.BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
349 "system/recovery.img", output_zip)
350 Item.Get("system/recovery.img", dir=False)
351
352 FixPermissions(script)
353
354 common.AddBoot(output_zip)
355 script.append("show_progress 0.2 0")
356 script.append("write_raw_image PACKAGE:boot.img BOOT:")
357 script.append("show_progress 0.2 10")
358
359 AddScript(script, output_zip)
360
361
362class File(object):
363 def __init__(self, name, data):
364 self.name = name
365 self.data = data
366 self.size = len(data)
367 self.sha1 = sha.sha(data).hexdigest()
368
369 def WriteToTemp(self):
370 t = tempfile.NamedTemporaryFile()
371 t.write(self.data)
372 t.flush()
373 return t
374
375 def AddToZip(self, z):
376 z.writestr(self.name, self.data)
377
378
379def LoadSystemFiles(z):
380 """Load all the files from SYSTEM/... in a given target-files
381 ZipFile, and return a dict of {filename: File object}."""
382 out = {}
383 for info in z.infolist():
384 if info.filename.startswith("SYSTEM/") and not IsSymlink(info):
385 fn = "system/" + info.filename[7:]
386 data = z.read(info.filename)
387 out[fn] = File(fn, data)
388 return out
389
390
391def Difference(tf, sf):
392 """Return the patch (as a string of data) needed to turn sf into tf."""
393
394 ttemp = tf.WriteToTemp()
395 stemp = sf.WriteToTemp()
396
397 ext = os.path.splitext(tf.name)[1]
398
399 try:
400 ptemp = tempfile.NamedTemporaryFile()
401 p = common.Run(["bsdiff", stemp.name, ttemp.name, ptemp.name])
402 _, err = p.communicate()
403 if err:
404 raise ExternalError("failure running bsdiff:\n%s\n" % (err,))
405 diff = ptemp.read()
406 ptemp.close()
407 finally:
408 stemp.close()
409 ttemp.close()
410
411 return diff
412
413
414def GetBuildProp(property, z):
415 """Return the fingerprint of the build of a given target-files
416 ZipFile object."""
417 bp = z.read("SYSTEM/build.prop")
418 if not property:
419 return bp
420 m = re.search(re.escape(property) + r"=(.*)\n", bp)
421 if not m:
422 raise ExternalException("couldn't find %s in build.prop" % (property,))
423 return m.group(1).strip()
424
425
426def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):
427 script = []
428
429 print "Loading target..."
430 target_data = LoadSystemFiles(target_zip)
431 print "Loading source..."
432 source_data = LoadSystemFiles(source_zip)
433
434 verbatim_targets = []
435 patch_list = []
436 largest_source_size = 0
437 for fn in sorted(target_data.keys()):
438 tf = target_data[fn]
439 sf = source_data.get(fn, None)
440
441 if sf is None or fn in OPTIONS.require_verbatim:
442 # This file should be included verbatim
443 if fn in OPTIONS.prohibit_verbatim:
444 raise ExternalError("\"%s\" must be sent verbatim" % (fn,))
445 print "send", fn, "verbatim"
446 tf.AddToZip(output_zip)
447 verbatim_targets.append((fn, tf.size))
448 elif tf.sha1 != sf.sha1:
449 # File is different; consider sending as a patch
450 d = Difference(tf, sf)
451 print fn, tf.size, len(d), (float(len(d)) / tf.size)
452 if len(d) > tf.size * OPTIONS.patch_threshold:
453 # patch is almost as big as the file; don't bother patching
454 tf.AddToZip(output_zip)
455 verbatim_targets.append((fn, tf.size))
456 else:
457 output_zip.writestr("patch/" + fn + ".p", d)
458 patch_list.append((fn, tf, sf, tf.size))
459 largest_source_size = max(largest_source_size, sf.size)
460 else:
461 # Target file identical to source.
462 pass
463
464 total_verbatim_size = sum([i[1] for i in verbatim_targets])
465 total_patched_size = sum([i[3] for i in patch_list])
466
467 source_fp = GetBuildProp("ro.build.fingerprint", source_zip)
468 target_fp = GetBuildProp("ro.build.fingerprint", target_zip)
469
470 script.append(('assert file_contains("SYSTEM:build.prop", '
471 '"ro.build.fingerprint=%s") == "true" || '
472 'file_contains("SYSTEM:build.prop", '
473 '"ro.build.fingerprint=%s") == "true"') %
474 (source_fp, target_fp))
475
476 source_boot = common.BuildBootableImage(
477 os.path.join(OPTIONS.source_tmp, "BOOT"))
478 target_boot = common.BuildBootableImage(
479 os.path.join(OPTIONS.target_tmp, "BOOT"))
480 updating_boot = (source_boot != target_boot)
481
482 source_recovery = common.BuildBootableImage(
483 os.path.join(OPTIONS.source_tmp, "RECOVERY"))
484 target_recovery = common.BuildBootableImage(
485 os.path.join(OPTIONS.target_tmp, "RECOVERY"))
486 updating_recovery = (source_recovery != target_recovery)
487
488 source_radio = source_zip.read("RADIO/image")
489 target_radio = target_zip.read("RADIO/image")
490 updating_radio = (source_radio != target_radio)
491
492 # The last 0.1 is reserved for creating symlinks, fixing
493 # permissions, and writing the boot image (if necessary).
494 progress_bar_total = 1.0
495 if updating_boot:
496 progress_bar_total -= 0.1
497 if updating_radio:
498 progress_bar_total -= 0.3
499
500 AppendAssertions(script, target_zip)
501
502 pb_verify = progress_bar_total * 0.3 * \
503 (total_patched_size /
504 float(total_patched_size+total_verbatim_size))
505
506 for i, (fn, tf, sf, size) in enumerate(patch_list):
507 if i % 5 == 0:
508 next_sizes = sum([i[3] for i in patch_list[i:i+5]])
509 script.append("show_progress %f 1" %
510 (next_sizes * pb_verify / total_patched_size,))
511 script.append("run_program PACKAGE:applypatch -c /%s %s %s" %
512 (fn, tf.sha1, sf.sha1))
513
514 if patch_list:
515 script.append("run_program PACKAGE:applypatch -s %d" %
516 (largest_source_size,))
517 script.append("copy_dir PACKAGE:patch CACHE:../tmp/patchtmp")
518 IncludeBinary("applypatch", target_zip, output_zip)
519
520 script.append("\n# ---- start making changes here\n")
521
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700522 if OPTIONS.wipe_user_data:
523 script.append("format DATA:")
524
Doug Zongkereef39442009-04-02 12:14:19 -0700525 DeleteFiles(script, [SubstituteRoot(i[0]) for i in verbatim_targets])
526
527 if updating_boot:
528 script.append("format BOOT:")
529 output_zip.writestr("boot.img", target_boot)
530 print "boot image changed; including."
531 else:
532 print "boot image unchanged; skipping."
533
534 if updating_recovery:
535 output_zip.writestr("system/recovery.img", target_recovery)
536 print "recovery image changed; including."
537 else:
538 print "recovery image unchanged; skipping."
539
540 if updating_radio:
541 script.append("show_progress 0.3 10")
542 script.append("write_radio_image PACKAGE:radio.img")
543 output_zip.writestr("radio.img", target_radio)
544 print "radio image changed; including."
545 else:
546 print "radio image unchanged; skipping."
547
548 pb_apply = progress_bar_total * 0.7 * \
549 (total_patched_size /
550 float(total_patched_size+total_verbatim_size))
551 for i, (fn, tf, sf, size) in enumerate(patch_list):
552 if i % 5 == 0:
553 next_sizes = sum([i[3] for i in patch_list[i:i+5]])
554 script.append("show_progress %f 1" %
555 (next_sizes * pb_apply / total_patched_size,))
556 script.append(("run_program PACKAGE:applypatch "
557 "/%s %s %d %s:/tmp/patchtmp/%s.p") %
558 (fn, tf.sha1, tf.size, sf.sha1, fn))
559
560 target_symlinks = CopySystemFiles(target_zip, None)
561
562 target_symlinks_d = dict([(i[1], i[0]) for i in target_symlinks])
563 temp_script = []
564 FixPermissions(temp_script)
565
566 # Note that this call will mess up the tree of Items, so make sure
567 # we're done with it.
568 source_symlinks = CopySystemFiles(source_zip, None)
569 source_symlinks_d = dict([(i[1], i[0]) for i in source_symlinks])
570
571 # Delete all the symlinks in source that aren't in target. This
572 # needs to happen before verbatim files are unpacked, in case a
573 # symlink in the source is replaced by a real file in the target.
574 to_delete = []
575 for dest, link in source_symlinks:
576 if link not in target_symlinks_d:
577 to_delete.append(link)
578 DeleteFiles(script, to_delete)
579
580 if verbatim_targets:
581 pb_verbatim = progress_bar_total * \
582 (total_verbatim_size /
583 float(total_patched_size+total_verbatim_size))
584 script.append("show_progress %f 5" % (pb_verbatim,))
585 script.append("copy_dir PACKAGE:system SYSTEM:")
586
587 # Create all the symlinks that don't already exist, or point to
588 # somewhere different than what we want. Delete each symlink before
589 # creating it, since the 'symlink' command won't overwrite.
590 to_create = []
591 for dest, link in target_symlinks:
592 if link in source_symlinks_d:
593 if dest != source_symlinks_d[link]:
594 to_create.append((dest, link))
595 else:
596 to_create.append((dest, link))
597 DeleteFiles(script, [i[1] for i in to_create])
598 script.extend(["symlink %s %s" % s for s in to_create])
599
600 # Now that the symlinks are created, we can set all the
601 # permissions.
602 script.extend(temp_script)
603
604 if updating_boot:
605 script.append("show_progress 0.1 5")
606 script.append("write_raw_image PACKAGE:boot.img BOOT:")
607
608 AddScript(script, output_zip)
609
610
611def main(argv):
612
613 def option_handler(o, a):
614 if o in ("-b", "--board_config"):
615 common.LoadBoardConfig(a)
Doug Zongkereef39442009-04-02 12:14:19 -0700616 elif o in ("-k", "--package_key"):
617 OPTIONS.package_key = a
Doug Zongkereef39442009-04-02 12:14:19 -0700618 elif o in ("-i", "--incremental_from"):
619 OPTIONS.incremental_source = a
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700620 elif o in ("-w", "--wipe_user_data"):
621 OPTIONS.wipe_user_data = True
Doug Zongkereef39442009-04-02 12:14:19 -0700622 else:
623 return False
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700624 return True
Doug Zongkereef39442009-04-02 12:14:19 -0700625
626 args = common.ParseOptions(argv, __doc__,
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700627 extra_opts="b:k:i:d:w",
Doug Zongkereef39442009-04-02 12:14:19 -0700628 extra_long_opts=["board_config=",
629 "package_key=",
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700630 "incremental_from=",
631 "wipe_user_data"],
Doug Zongkereef39442009-04-02 12:14:19 -0700632 extra_option_handler=option_handler)
633
634 if len(args) != 2:
635 common.Usage(__doc__)
636 sys.exit(1)
637
638 if not OPTIONS.max_image_size:
639 print
640 print " WARNING: No board config specified; will not check image"
641 print " sizes against limits. Use -b to make sure the generated"
642 print " images don't exceed partition sizes."
643 print
644
645 print "unzipping target target-files..."
646 OPTIONS.input_tmp = common.UnzipTemp(args[0])
647 OPTIONS.target_tmp = OPTIONS.input_tmp
648 input_zip = zipfile.ZipFile(args[0], "r")
649 if OPTIONS.package_key:
650 temp_zip_file = tempfile.NamedTemporaryFile()
651 output_zip = zipfile.ZipFile(temp_zip_file, "w",
652 compression=zipfile.ZIP_DEFLATED)
653 else:
654 output_zip = zipfile.ZipFile(args[1], "w",
655 compression=zipfile.ZIP_DEFLATED)
656
657 if OPTIONS.incremental_source is None:
658 WriteFullOTAPackage(input_zip, output_zip)
659 else:
660 print "unzipping source target-files..."
661 OPTIONS.source_tmp = common.UnzipTemp(OPTIONS.incremental_source)
662 source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r")
663 WriteIncrementalOTAPackage(input_zip, source_zip, output_zip)
664
665 output_zip.close()
666 if OPTIONS.package_key:
667 SignOutput(temp_zip_file.name, args[1])
668 temp_zip_file.close()
669
670 common.Cleanup()
671
672 print "done."
673
674
675if __name__ == '__main__':
676 try:
677 main(sys.argv[1:])
678 except common.ExternalError, e:
679 print
680 print " ERROR: %s" % (e,)
681 print
682 sys.exit(1)