blob: cf5fb34834e52eb58cb0beae654a65f26791ba46 [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>
Doug Zongkerfdd8e692009-08-03 17:27:48 -070025 Deprecated.
Doug Zongkereef39442009-04-02 12:14:19 -070026
27 -k (--package_key) <key>
28 Key to use to sign the package (default is
29 "build/target/product/security/testkey").
30
31 -i (--incremental_from) <file>
32 Generate an incremental OTA using the given target-files zip as
33 the starting build.
34
Doug Zongkerdbfaae52009-04-21 17:12:54 -070035 -w (--wipe_user_data)
36 Generate an OTA package that will wipe the user data partition
37 when installed.
38
Doug Zongker962069c2009-04-23 11:41:58 -070039 -n (--no_prereq)
40 Omit the timestamp prereq check normally included at the top of
41 the build scripts (used for developer OTA packages which
42 legitimately need to go back and forth).
43
Doug Zongker1c390a22009-05-14 19:06:36 -070044 -e (--extra_script) <file>
45 Insert the contents of file at the end of the update script.
46
Doug Zongkerc494d7c2009-06-18 08:43:44 -070047 -m (--script_mode) <mode>
48 Specify 'amend' or 'edify' scripts, or 'auto' to pick
49 automatically (this is the default).
50
Doug Zongkereef39442009-04-02 12:14:19 -070051"""
52
53import sys
54
55if sys.hexversion < 0x02040000:
56 print >> sys.stderr, "Python 2.4 or newer is required."
57 sys.exit(1)
58
59import copy
Doug Zongkerc18736b2009-09-30 09:20:32 -070060import errno
Doug Zongkereef39442009-04-02 12:14:19 -070061import os
62import re
63import sha
64import subprocess
65import tempfile
Doug Zongker761e6422009-09-25 10:45:39 -070066import threading
Doug Zongkereef39442009-04-02 12:14:19 -070067import time
68import zipfile
69
70import common
Doug Zongkerc494d7c2009-06-18 08:43:44 -070071import amend_generator
72import edify_generator
Doug Zongker03061472009-07-13 18:36:37 -070073import both_generator
Doug Zongkereef39442009-04-02 12:14:19 -070074
75OPTIONS = common.OPTIONS
76OPTIONS.package_key = "build/target/product/security/testkey"
77OPTIONS.incremental_source = None
78OPTIONS.require_verbatim = set()
79OPTIONS.prohibit_verbatim = set(("system/build.prop",))
80OPTIONS.patch_threshold = 0.95
Doug Zongkerdbfaae52009-04-21 17:12:54 -070081OPTIONS.wipe_user_data = False
Doug Zongker962069c2009-04-23 11:41:58 -070082OPTIONS.omit_prereq = False
Doug Zongker1c390a22009-05-14 19:06:36 -070083OPTIONS.extra_script = None
Doug Zongkerc494d7c2009-06-18 08:43:44 -070084OPTIONS.script_mode = 'auto'
Doug Zongker761e6422009-09-25 10:45:39 -070085OPTIONS.worker_threads = 3
Doug Zongkereef39442009-04-02 12:14:19 -070086
87def MostPopularKey(d, default):
88 """Given a dict, return the key corresponding to the largest
89 value. Returns 'default' if the dict is empty."""
90 x = [(v, k) for (k, v) in d.iteritems()]
91 if not x: return default
92 x.sort()
93 return x[-1][1]
94
95
96def IsSymlink(info):
97 """Return true if the zipfile.ZipInfo object passed in represents a
98 symlink."""
99 return (info.external_attr >> 16) == 0120777
100
101
102
103class Item:
104 """Items represent the metadata (user, group, mode) of files and
105 directories in the system image."""
106 ITEMS = {}
107 def __init__(self, name, dir=False):
108 self.name = name
109 self.uid = None
110 self.gid = None
111 self.mode = None
112 self.dir = dir
113
114 if name:
115 self.parent = Item.Get(os.path.dirname(name), dir=True)
116 self.parent.children.append(self)
117 else:
118 self.parent = None
119 if dir:
120 self.children = []
121
122 def Dump(self, indent=0):
123 if self.uid is not None:
124 print "%s%s %d %d %o" % (" "*indent, self.name, self.uid, self.gid, self.mode)
125 else:
126 print "%s%s %s %s %s" % (" "*indent, self.name, self.uid, self.gid, self.mode)
127 if self.dir:
128 print "%s%s" % (" "*indent, self.descendants)
129 print "%s%s" % (" "*indent, self.best_subtree)
130 for i in self.children:
131 i.Dump(indent=indent+1)
132
133 @classmethod
134 def Get(cls, name, dir=False):
135 if name not in cls.ITEMS:
136 cls.ITEMS[name] = Item(name, dir=dir)
137 return cls.ITEMS[name]
138
139 @classmethod
Doug Zongker283e2a12010-03-15 17:52:32 -0700140 def GetMetadata(cls, input_zip):
141
142 try:
143 # See if the target_files contains a record of what the uid,
144 # gid, and mode is supposed to be.
145 output = input_zip.read("META/filesystem_config.txt")
146 except KeyError:
147 # Run the external 'fs_config' program to determine the desired
148 # uid, gid, and mode for every Item object. Note this uses the
149 # one in the client now, which might not be the same as the one
150 # used when this target_files was built.
151 p = common.Run(["fs_config"], stdin=subprocess.PIPE,
152 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
153 suffix = { False: "", True: "/" }
154 input = "".join(["%s%s\n" % (i.name, suffix[i.dir])
155 for i in cls.ITEMS.itervalues() if i.name])
Doug Zongker3475d362010-03-17 16:39:30 -0700156 output, error = p.communicate(input)
Doug Zongker283e2a12010-03-15 17:52:32 -0700157 assert not error
Doug Zongkereef39442009-04-02 12:14:19 -0700158
159 for line in output.split("\n"):
160 if not line: continue
161 name, uid, gid, mode = line.split()
Doug Zongker283e2a12010-03-15 17:52:32 -0700162 i = cls.ITEMS.get(name, None)
163 if i is not None:
164 i.uid = int(uid)
165 i.gid = int(gid)
166 i.mode = int(mode, 8)
167 if i.dir:
168 i.children.sort(key=lambda i: i.name)
169
170 # set metadata for the files generated by this script.
171 i = cls.ITEMS.get("system/recovery-from-boot.p", None)
172 if i: i.uid, i.gid, i.mode = 0, 0, 0644
173 i = cls.ITEMS.get("system/etc/install-recovery.sh", None)
174 if i: i.uid, i.gid, i.mode = 0, 0, 0544
Doug Zongkereef39442009-04-02 12:14:19 -0700175
176 def CountChildMetadata(self):
177 """Count up the (uid, gid, mode) tuples for all children and
178 determine the best strategy for using set_perm_recursive and
179 set_perm to correctly chown/chmod all the files to their desired
180 values. Recursively calls itself for all descendants.
181
182 Returns a dict of {(uid, gid, dmode, fmode): count} counting up
183 all descendants of this node. (dmode or fmode may be None.) Also
184 sets the best_subtree of each directory Item to the (uid, gid,
185 dmode, fmode) tuple that will match the most descendants of that
186 Item.
187 """
188
189 assert self.dir
190 d = self.descendants = {(self.uid, self.gid, self.mode, None): 1}
191 for i in self.children:
192 if i.dir:
193 for k, v in i.CountChildMetadata().iteritems():
194 d[k] = d.get(k, 0) + v
195 else:
196 k = (i.uid, i.gid, None, i.mode)
197 d[k] = d.get(k, 0) + 1
198
199 # Find the (uid, gid, dmode, fmode) tuple that matches the most
200 # descendants.
201
202 # First, find the (uid, gid) pair that matches the most
203 # descendants.
204 ug = {}
205 for (uid, gid, _, _), count in d.iteritems():
206 ug[(uid, gid)] = ug.get((uid, gid), 0) + count
207 ug = MostPopularKey(ug, (0, 0))
208
209 # Now find the dmode and fmode that match the most descendants
210 # with that (uid, gid), and choose those.
211 best_dmode = (0, 0755)
212 best_fmode = (0, 0644)
213 for k, count in d.iteritems():
214 if k[:2] != ug: continue
215 if k[2] is not None and count >= best_dmode[0]: best_dmode = (count, k[2])
216 if k[3] is not None and count >= best_fmode[0]: best_fmode = (count, k[3])
217 self.best_subtree = ug + (best_dmode[1], best_fmode[1])
218
219 return d
220
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700221 def SetPermissions(self, script):
Doug Zongkereef39442009-04-02 12:14:19 -0700222 """Append set_perm/set_perm_recursive commands to 'script' to
223 set all permissions, users, and groups for the tree of files
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700224 rooted at 'self'."""
Doug Zongkereef39442009-04-02 12:14:19 -0700225
226 self.CountChildMetadata()
227
228 def recurse(item, current):
229 # current is the (uid, gid, dmode, fmode) tuple that the current
230 # item (and all its children) have already been set to. We only
231 # need to issue set_perm/set_perm_recursive commands if we're
232 # supposed to be something different.
233 if item.dir:
234 if current != item.best_subtree:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700235 script.SetPermissionsRecursive("/"+item.name, *item.best_subtree)
Doug Zongkereef39442009-04-02 12:14:19 -0700236 current = item.best_subtree
237
238 if item.uid != current[0] or item.gid != current[1] or \
239 item.mode != current[2]:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700240 script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode)
Doug Zongkereef39442009-04-02 12:14:19 -0700241
242 for i in item.children:
243 recurse(i, current)
244 else:
245 if item.uid != current[0] or item.gid != current[1] or \
246 item.mode != current[3]:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700247 script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode)
Doug Zongkereef39442009-04-02 12:14:19 -0700248
249 recurse(self, (-1, -1, -1, -1))
250
251
252def CopySystemFiles(input_zip, output_zip=None,
253 substitute=None):
254 """Copies files underneath system/ in the input zip to the output
255 zip. Populates the Item class with their metadata, and returns a
256 list of symlinks. output_zip may be None, in which case the copy is
257 skipped (but the other side effects still happen). substitute is an
258 optional dict of {output filename: contents} to be output instead of
259 certain input files.
260 """
261
262 symlinks = []
263
264 for info in input_zip.infolist():
265 if info.filename.startswith("SYSTEM/"):
266 basefilename = info.filename[7:]
267 if IsSymlink(info):
268 symlinks.append((input_zip.read(info.filename),
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700269 "/system/" + basefilename))
Doug Zongkereef39442009-04-02 12:14:19 -0700270 else:
271 info2 = copy.copy(info)
272 fn = info2.filename = "system/" + basefilename
273 if substitute and fn in substitute and substitute[fn] is None:
274 continue
275 if output_zip is not None:
276 if substitute and fn in substitute:
277 data = substitute[fn]
278 else:
279 data = input_zip.read(info.filename)
280 output_zip.writestr(info2, data)
281 if fn.endswith("/"):
282 Item.Get(fn[:-1], dir=True)
283 else:
284 Item.Get(fn, dir=False)
285
286 symlinks.sort()
287 return symlinks
288
289
Doug Zongkereef39442009-04-02 12:14:19 -0700290def SignOutput(temp_zip_name, output_zip_name):
291 key_passwords = common.GetKeyPasswords([OPTIONS.package_key])
292 pw = key_passwords[OPTIONS.package_key]
293
Doug Zongker951495f2009-08-14 12:44:19 -0700294 common.SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
295 whole_file=True)
Doug Zongkereef39442009-04-02 12:14:19 -0700296
297
Doug Zongkereef39442009-04-02 12:14:19 -0700298def AppendAssertions(script, input_zip):
Doug Zongkereef39442009-04-02 12:14:19 -0700299 device = GetBuildProp("ro.product.device", input_zip)
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700300 script.AssertDevice(device)
Doug Zongkereef39442009-04-02 12:14:19 -0700301
Doug Zongkereef39442009-04-02 12:14:19 -0700302
Doug Zongker73ef8252009-07-23 15:12:53 -0700303def MakeRecoveryPatch(output_zip, recovery_img, boot_img):
304 """Generate a binary patch that creates the recovery image starting
305 with the boot image. (Most of the space in these images is just the
306 kernel, which is identical for the two, so the resulting patch
307 should be efficient.) Add it to the output zip, along with a shell
308 script that is run from init.rc on first boot to actually do the
309 patching and install the new recovery image.
310
311 recovery_img and boot_img should be File objects for the
312 corresponding images.
313
314 Returns an Item for the shell script, which must be made
315 executable.
316 """
317
Doug Zongker761e6422009-09-25 10:45:39 -0700318 d = Difference(recovery_img, boot_img)
319 _, _, patch = d.ComputePatch()
Doug Zongkercfd7db62009-10-07 11:35:53 -0700320 common.ZipWriteStr(output_zip, "recovery/recovery-from-boot.p", patch)
Doug Zongker73ef8252009-07-23 15:12:53 -0700321 Item.Get("system/recovery-from-boot.p", dir=False)
322
323 # Images with different content will have a different first page, so
324 # we check to see if this recovery has already been installed by
325 # testing just the first 2k.
326 HEADER_SIZE = 2048
327 header_sha1 = sha.sha(recovery_img.data[:HEADER_SIZE]).hexdigest()
328 sh = """#!/system/bin/sh
329if ! applypatch -c MTD:recovery:%(header_size)d:%(header_sha1)s; then
330 log -t recovery "Installing new recovery image"
331 applypatch MTD:boot:%(boot_size)d:%(boot_sha1)s MTD:recovery %(recovery_sha1)s %(recovery_size)d %(boot_sha1)s:/system/recovery-from-boot.p
332else
333 log -t recovery "Recovery image already installed"
334fi
335""" % { 'boot_size': boot_img.size,
336 'boot_sha1': boot_img.sha1,
337 'header_size': HEADER_SIZE,
338 'header_sha1': header_sha1,
339 'recovery_size': recovery_img.size,
340 'recovery_sha1': recovery_img.sha1 }
Doug Zongkercfd7db62009-10-07 11:35:53 -0700341 common.ZipWriteStr(output_zip, "recovery/etc/install-recovery.sh", sh)
Doug Zongker73ef8252009-07-23 15:12:53 -0700342 return Item.Get("system/etc/install-recovery.sh", dir=False)
343
344
Doug Zongkereef39442009-04-02 12:14:19 -0700345def WriteFullOTAPackage(input_zip, output_zip):
Doug Zongker03061472009-07-13 18:36:37 -0700346 if OPTIONS.script_mode == "auto":
347 script = both_generator.BothGenerator(2)
348 elif OPTIONS.script_mode == "amend":
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700349 script = amend_generator.AmendGenerator()
350 else:
351 # TODO: how to determine this? We don't know what version it will
352 # be installed on top of. For now, we expect the API just won't
353 # change very often.
Doug Zongker03061472009-07-13 18:36:37 -0700354 script = edify_generator.EdifyGenerator(2)
Doug Zongkereef39442009-04-02 12:14:19 -0700355
Doug Zongker2ea21062010-04-28 16:05:21 -0700356 metadata = {"post-build": GetBuildProp("ro.build.fingerprint", input_zip),
357 "pre-device": GetBuildProp("ro.product.device", input_zip),
358 }
359
Doug Zongker05d3dea2009-06-22 11:32:31 -0700360 device_specific = common.DeviceSpecificParams(
361 input_zip=input_zip,
Doug Zongker14833602010-02-02 13:12:04 -0800362 input_version=GetRecoveryAPIVersion(input_zip),
Doug Zongker05d3dea2009-06-22 11:32:31 -0700363 output_zip=output_zip,
364 script=script,
Doug Zongker2ea21062010-04-28 16:05:21 -0700365 input_tmp=OPTIONS.input_tmp,
366 metadata=metadata)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700367
Doug Zongker962069c2009-04-23 11:41:58 -0700368 if not OPTIONS.omit_prereq:
369 ts = GetBuildProp("ro.build.date.utc", input_zip)
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700370 script.AssertOlderBuild(ts)
Doug Zongkereef39442009-04-02 12:14:19 -0700371
372 AppendAssertions(script, input_zip)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700373 device_specific.FullOTA_Assertions()
Doug Zongker171f1cd2009-06-15 22:36:37 -0700374
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700375 script.ShowProgress(0.5, 0)
Doug Zongkereef39442009-04-02 12:14:19 -0700376
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700377 if OPTIONS.wipe_user_data:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700378 script.FormatPartition("userdata")
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700379
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700380 script.FormatPartition("system")
381 script.Mount("MTD", "system", "/system")
Doug Zongkercfd7db62009-10-07 11:35:53 -0700382 script.UnpackPackageDir("recovery", "/system")
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700383 script.UnpackPackageDir("system", "/system")
Doug Zongkereef39442009-04-02 12:14:19 -0700384
385 symlinks = CopySystemFiles(input_zip, output_zip)
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700386 script.MakeSymlinks(symlinks)
Doug Zongkereef39442009-04-02 12:14:19 -0700387
Doug Zongker73ef8252009-07-23 15:12:53 -0700388 boot_img = File("boot.img", common.BuildBootableImage(
389 os.path.join(OPTIONS.input_tmp, "BOOT")))
390 recovery_img = File("recovery.img", common.BuildBootableImage(
391 os.path.join(OPTIONS.input_tmp, "RECOVERY")))
Doug Zongker283e2a12010-03-15 17:52:32 -0700392 MakeRecoveryPatch(output_zip, recovery_img, boot_img)
Doug Zongkereef39442009-04-02 12:14:19 -0700393
Doug Zongker283e2a12010-03-15 17:52:32 -0700394 Item.GetMetadata(input_zip)
Doug Zongker73ef8252009-07-23 15:12:53 -0700395 Item.Get("system").SetPermissions(script)
396
397 common.CheckSize(boot_img.data, "boot.img")
398 common.ZipWriteStr(output_zip, "boot.img", boot_img.data)
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700399 script.ShowProgress(0.2, 0)
400
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700401 script.ShowProgress(0.2, 10)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700402 script.WriteRawImage("boot", "boot.img")
403
404 script.ShowProgress(0.1, 0)
405 device_specific.FullOTA_InstallEnd()
Doug Zongkereef39442009-04-02 12:14:19 -0700406
Doug Zongker1c390a22009-05-14 19:06:36 -0700407 if OPTIONS.extra_script is not None:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700408 script.AppendExtra(OPTIONS.extra_script)
Doug Zongker1c390a22009-05-14 19:06:36 -0700409
Doug Zongker14833602010-02-02 13:12:04 -0800410 script.UnmountAll()
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700411 script.AddToZip(input_zip, output_zip)
Doug Zongker2ea21062010-04-28 16:05:21 -0700412 WriteMetadata(metadata, output_zip)
413
414
415def WriteMetadata(metadata, output_zip):
416 common.ZipWriteStr(output_zip, "META-INF/com/android/metadata",
417 "".join(["%s=%s\n" % kv
418 for kv in sorted(metadata.iteritems())]))
Doug Zongkereef39442009-04-02 12:14:19 -0700419
420
421class File(object):
422 def __init__(self, name, data):
423 self.name = name
424 self.data = data
425 self.size = len(data)
426 self.sha1 = sha.sha(data).hexdigest()
427
428 def WriteToTemp(self):
429 t = tempfile.NamedTemporaryFile()
430 t.write(self.data)
431 t.flush()
432 return t
433
434 def AddToZip(self, z):
Doug Zongker048e7ca2009-06-15 14:31:53 -0700435 common.ZipWriteStr(z, self.name, self.data)
Doug Zongkereef39442009-04-02 12:14:19 -0700436
437
438def LoadSystemFiles(z):
439 """Load all the files from SYSTEM/... in a given target-files
440 ZipFile, and return a dict of {filename: File object}."""
441 out = {}
442 for info in z.infolist():
443 if info.filename.startswith("SYSTEM/") and not IsSymlink(info):
444 fn = "system/" + info.filename[7:]
445 data = z.read(info.filename)
446 out[fn] = File(fn, data)
447 return out
448
449
Doug Zongker761e6422009-09-25 10:45:39 -0700450DIFF_PROGRAM_BY_EXT = {
451 ".gz" : "imgdiff",
452 ".zip" : ["imgdiff", "-z"],
453 ".jar" : ["imgdiff", "-z"],
454 ".apk" : ["imgdiff", "-z"],
455 ".img" : "imgdiff",
456 }
Doug Zongkereef39442009-04-02 12:14:19 -0700457
Doug Zongkereef39442009-04-02 12:14:19 -0700458
Doug Zongker761e6422009-09-25 10:45:39 -0700459class Difference(object):
460 def __init__(self, tf, sf):
461 self.tf = tf
462 self.sf = sf
463 self.patch = None
Doug Zongkereef39442009-04-02 12:14:19 -0700464
Doug Zongker761e6422009-09-25 10:45:39 -0700465 def ComputePatch(self):
466 """Compute the patch (as a string of data) needed to turn sf into
467 tf. Returns the same tuple as GetPatch()."""
Doug Zongkereef39442009-04-02 12:14:19 -0700468
Doug Zongker761e6422009-09-25 10:45:39 -0700469 tf = self.tf
470 sf = self.sf
471
472 ext = os.path.splitext(tf.name)[1]
473 diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff")
474
475 ttemp = tf.WriteToTemp()
476 stemp = sf.WriteToTemp()
477
478 ext = os.path.splitext(tf.name)[1]
479
480 try:
481 ptemp = tempfile.NamedTemporaryFile()
482 if isinstance(diff_program, list):
483 cmd = copy.copy(diff_program)
484 else:
485 cmd = [diff_program]
486 cmd.append(stemp.name)
487 cmd.append(ttemp.name)
488 cmd.append(ptemp.name)
489 p = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
490 _, err = p.communicate()
491 if err or p.returncode != 0:
492 print "WARNING: failure running %s:\n%s\n" % (diff_program, err)
493 return None
494 diff = ptemp.read()
495 finally:
496 ptemp.close()
497 stemp.close()
498 ttemp.close()
499
500 self.patch = diff
501 return self.tf, self.sf, self.patch
502
503
504 def GetPatch(self):
505 """Return a tuple (target_file, source_file, patch_data).
506 patch_data may be None if ComputePatch hasn't been called, or if
507 computing the patch failed."""
508 return self.tf, self.sf, self.patch
509
510
511def ComputeDifferences(diffs):
512 """Call ComputePatch on all the Difference objects in 'diffs'."""
513 print len(diffs), "diffs to compute"
514
515 # Do the largest files first, to try and reduce the long-pole effect.
516 by_size = [(i.tf.size, i) for i in diffs]
517 by_size.sort(reverse=True)
518 by_size = [i[1] for i in by_size]
519
520 lock = threading.Lock()
521 diff_iter = iter(by_size) # accessed under lock
522
523 def worker():
524 try:
525 lock.acquire()
526 for d in diff_iter:
527 lock.release()
528 start = time.time()
529 d.ComputePatch()
530 dur = time.time() - start
531 lock.acquire()
532
533 tf, sf, patch = d.GetPatch()
534 if sf.name == tf.name:
535 name = tf.name
536 else:
537 name = "%s (%s)" % (tf.name, sf.name)
538 if patch is None:
539 print "patching failed! %s" % (name,)
540 else:
541 print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % (
542 dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name)
543 lock.release()
Doug Zongker481c4e62009-09-28 10:07:13 -0700544 except Exception, e:
Doug Zongker761e6422009-09-25 10:45:39 -0700545 print e
546 raise
547
548 # start worker threads; wait for them all to finish.
549 threads = [threading.Thread(target=worker)
550 for i in range(OPTIONS.worker_threads)]
551 for th in threads:
552 th.start()
553 while threads:
554 threads.pop().join()
Doug Zongkereef39442009-04-02 12:14:19 -0700555
556
557def GetBuildProp(property, z):
558 """Return the fingerprint of the build of a given target-files
559 ZipFile object."""
560 bp = z.read("SYSTEM/build.prop")
561 if not property:
562 return bp
563 m = re.search(re.escape(property) + r"=(.*)\n", bp)
564 if not m:
Doug Zongker9fc74c72009-06-23 16:27:38 -0700565 raise common.ExternalError("couldn't find %s in build.prop" % (property,))
Doug Zongkereef39442009-04-02 12:14:19 -0700566 return m.group(1).strip()
567
568
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700569def GetRecoveryAPIVersion(zip):
570 """Returns the version of the recovery API. Version 0 is the older
571 amend code (no separate binary)."""
572 try:
573 version = zip.read("META/recovery-api-version.txt")
574 return int(version)
575 except KeyError:
576 try:
577 # version one didn't have the recovery-api-version.txt file, but
578 # it did include an updater binary.
579 zip.getinfo("OTA/bin/updater")
580 return 1
581 except KeyError:
582 return 0
583
Doug Zongker15604b82009-09-01 17:53:34 -0700584
Doug Zongkereef39442009-04-02 12:14:19 -0700585def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700586 source_version = GetRecoveryAPIVersion(source_zip)
Doug Zongker14833602010-02-02 13:12:04 -0800587 target_version = GetRecoveryAPIVersion(target_zip)
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700588
589 if OPTIONS.script_mode == 'amend':
590 script = amend_generator.AmendGenerator()
591 elif OPTIONS.script_mode == 'edify':
592 if source_version == 0:
593 print ("WARNING: generating edify script for a source that "
594 "can't install it.")
595 script = edify_generator.EdifyGenerator(source_version)
596 elif OPTIONS.script_mode == 'auto':
597 if source_version > 0:
598 script = edify_generator.EdifyGenerator(source_version)
599 else:
600 script = amend_generator.AmendGenerator()
601 else:
602 raise ValueError('unknown script mode "%s"' % (OPTIONS.script_mode,))
Doug Zongkereef39442009-04-02 12:14:19 -0700603
Doug Zongker2ea21062010-04-28 16:05:21 -0700604 metadata = {"pre-device": GetBuildProp("ro.product.device", source_zip),
605 }
606
Doug Zongker05d3dea2009-06-22 11:32:31 -0700607 device_specific = common.DeviceSpecificParams(
608 source_zip=source_zip,
Doug Zongker14833602010-02-02 13:12:04 -0800609 source_version=source_version,
Doug Zongker05d3dea2009-06-22 11:32:31 -0700610 target_zip=target_zip,
Doug Zongker14833602010-02-02 13:12:04 -0800611 target_version=target_version,
Doug Zongker05d3dea2009-06-22 11:32:31 -0700612 output_zip=output_zip,
Doug Zongker2ea21062010-04-28 16:05:21 -0700613 script=script,
614 metadata=metadata)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700615
Doug Zongkereef39442009-04-02 12:14:19 -0700616 print "Loading target..."
617 target_data = LoadSystemFiles(target_zip)
618 print "Loading source..."
619 source_data = LoadSystemFiles(source_zip)
620
621 verbatim_targets = []
622 patch_list = []
Doug Zongker761e6422009-09-25 10:45:39 -0700623 diffs = []
Doug Zongkereef39442009-04-02 12:14:19 -0700624 largest_source_size = 0
625 for fn in sorted(target_data.keys()):
626 tf = target_data[fn]
Doug Zongker761e6422009-09-25 10:45:39 -0700627 assert fn == tf.name
Doug Zongkereef39442009-04-02 12:14:19 -0700628 sf = source_data.get(fn, None)
629
630 if sf is None or fn in OPTIONS.require_verbatim:
631 # This file should be included verbatim
632 if fn in OPTIONS.prohibit_verbatim:
Doug Zongker9fc74c72009-06-23 16:27:38 -0700633 raise common.ExternalError("\"%s\" must be sent verbatim" % (fn,))
Doug Zongkereef39442009-04-02 12:14:19 -0700634 print "send", fn, "verbatim"
635 tf.AddToZip(output_zip)
636 verbatim_targets.append((fn, tf.size))
637 elif tf.sha1 != sf.sha1:
638 # File is different; consider sending as a patch
Doug Zongker761e6422009-09-25 10:45:39 -0700639 diffs.append(Difference(tf, sf))
Doug Zongkereef39442009-04-02 12:14:19 -0700640 else:
641 # Target file identical to source.
642 pass
643
Doug Zongker761e6422009-09-25 10:45:39 -0700644 ComputeDifferences(diffs)
645
646 for diff in diffs:
647 tf, sf, d = diff.GetPatch()
648 if d is None or len(d) > tf.size * OPTIONS.patch_threshold:
649 # patch is almost as big as the file; don't bother patching
650 tf.AddToZip(output_zip)
651 verbatim_targets.append((tf.name, tf.size))
652 else:
653 common.ZipWriteStr(output_zip, "patch/" + tf.name + ".p", d)
Doug Zongker5a482092010-02-17 16:09:18 -0800654 patch_list.append((tf.name, tf, sf, tf.size, sha.sha(d).hexdigest()))
Doug Zongker761e6422009-09-25 10:45:39 -0700655 largest_source_size = max(largest_source_size, sf.size)
Doug Zongkereef39442009-04-02 12:14:19 -0700656
657 source_fp = GetBuildProp("ro.build.fingerprint", source_zip)
658 target_fp = GetBuildProp("ro.build.fingerprint", target_zip)
Doug Zongker2ea21062010-04-28 16:05:21 -0700659 metadata["pre-build"] = source_fp
660 metadata["post-build"] = target_fp
Doug Zongkereef39442009-04-02 12:14:19 -0700661
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700662 script.Mount("MTD", "system", "/system")
663 script.AssertSomeFingerprint(source_fp, target_fp)
Doug Zongkereef39442009-04-02 12:14:19 -0700664
Doug Zongker5da317e2009-06-02 13:38:17 -0700665 source_boot = File("/tmp/boot.img",
666 common.BuildBootableImage(
667 os.path.join(OPTIONS.source_tmp, "BOOT")))
668 target_boot = File("/tmp/boot.img",
669 common.BuildBootableImage(
670 os.path.join(OPTIONS.target_tmp, "BOOT")))
671 updating_boot = (source_boot.data != target_boot.data)
Doug Zongkereef39442009-04-02 12:14:19 -0700672
Doug Zongkerf6a8bad2009-05-29 11:41:21 -0700673 source_recovery = File("system/recovery.img",
674 common.BuildBootableImage(
675 os.path.join(OPTIONS.source_tmp, "RECOVERY")))
676 target_recovery = File("system/recovery.img",
677 common.BuildBootableImage(
678 os.path.join(OPTIONS.target_tmp, "RECOVERY")))
679 updating_recovery = (source_recovery.data != target_recovery.data)
Doug Zongkereef39442009-04-02 12:14:19 -0700680
Doug Zongker881dd402009-09-20 14:03:55 -0700681 # Here's how we divide up the progress bar:
682 # 0.1 for verifying the start state (PatchCheck calls)
683 # 0.8 for applying patches (ApplyPatch calls)
684 # 0.1 for unpacking verbatim files, symlinking, and doing the
685 # device-specific commands.
Doug Zongkereef39442009-04-02 12:14:19 -0700686
687 AppendAssertions(script, target_zip)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700688 device_specific.IncrementalOTA_Assertions()
Doug Zongkereef39442009-04-02 12:14:19 -0700689
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700690 script.Print("Verifying current system...")
691
Doug Zongker881dd402009-09-20 14:03:55 -0700692 script.ShowProgress(0.1, 0)
693 total_verify_size = float(sum([i[2].size for i in patch_list]) + 1)
694 if updating_boot:
695 total_verify_size += source_boot.size
696 so_far = 0
Doug Zongkereef39442009-04-02 12:14:19 -0700697
Doug Zongker5a482092010-02-17 16:09:18 -0800698 for fn, tf, sf, size, patch_sha in patch_list:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700699 script.PatchCheck("/"+fn, tf.sha1, sf.sha1)
Doug Zongker881dd402009-09-20 14:03:55 -0700700 so_far += sf.size
701 script.SetProgress(so_far / total_verify_size)
Doug Zongkereef39442009-04-02 12:14:19 -0700702
Doug Zongker5da317e2009-06-02 13:38:17 -0700703 if updating_boot:
Doug Zongker761e6422009-09-25 10:45:39 -0700704 d = Difference(target_boot, source_boot)
705 _, _, d = d.ComputePatch()
Doug Zongker5da317e2009-06-02 13:38:17 -0700706 print "boot target: %d source: %d diff: %d" % (
707 target_boot.size, source_boot.size, len(d))
708
Doug Zongker048e7ca2009-06-15 14:31:53 -0700709 common.ZipWriteStr(output_zip, "patch/boot.img.p", d)
Doug Zongker5da317e2009-06-02 13:38:17 -0700710
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700711 script.PatchCheck("MTD:boot:%d:%s:%d:%s" %
712 (source_boot.size, source_boot.sha1,
713 target_boot.size, target_boot.sha1))
Doug Zongker881dd402009-09-20 14:03:55 -0700714 so_far += source_boot.size
715 script.SetProgress(so_far / total_verify_size)
Doug Zongker5da317e2009-06-02 13:38:17 -0700716
717 if patch_list or updating_recovery or updating_boot:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700718 script.CacheFreeSpaceCheck(largest_source_size)
Doug Zongker5a482092010-02-17 16:09:18 -0800719
Doug Zongker05d3dea2009-06-22 11:32:31 -0700720 device_specific.IncrementalOTA_VerifyEnd()
721
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700722 script.Comment("---- start making changes here ----")
Doug Zongkereef39442009-04-02 12:14:19 -0700723
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700724 if OPTIONS.wipe_user_data:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700725 script.Print("Erasing user data...")
726 script.FormatPartition("userdata")
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700727
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700728 script.Print("Removing unneeded files...")
Doug Zongker0f3298a2009-06-30 08:16:58 -0700729 script.DeleteFiles(["/"+i[0] for i in verbatim_targets] +
730 ["/"+i for i in sorted(source_data)
Doug Zongker3b949f02009-08-24 10:24:32 -0700731 if i not in target_data] +
732 ["/system/recovery.img"])
Doug Zongkereef39442009-04-02 12:14:19 -0700733
Doug Zongker881dd402009-09-20 14:03:55 -0700734 script.ShowProgress(0.8, 0)
735 total_patch_size = float(sum([i[1].size for i in patch_list]) + 1)
736 if updating_boot:
737 total_patch_size += target_boot.size
738 so_far = 0
739
740 script.Print("Patching system files...")
Doug Zongker5a482092010-02-17 16:09:18 -0800741 for fn, tf, sf, size, _ in patch_list:
Doug Zongkerc8d446b2010-02-22 15:41:53 -0800742 script.ApplyPatch("/"+fn, "-", tf.size, tf.sha1, sf.sha1, "patch/"+fn+".p")
Doug Zongker881dd402009-09-20 14:03:55 -0700743 so_far += tf.size
744 script.SetProgress(so_far / total_patch_size)
745
Doug Zongkereef39442009-04-02 12:14:19 -0700746 if updating_boot:
Doug Zongker5da317e2009-06-02 13:38:17 -0700747 # Produce the boot image by applying a patch to the current
748 # contents of the boot partition, and write it back to the
749 # partition.
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700750 script.Print("Patching boot image...")
751 script.ApplyPatch("MTD:boot:%d:%s:%d:%s"
752 % (source_boot.size, source_boot.sha1,
753 target_boot.size, target_boot.sha1),
754 "-",
755 target_boot.size, target_boot.sha1,
Doug Zongkerc8d446b2010-02-22 15:41:53 -0800756 source_boot.sha1, "patch/boot.img.p")
Doug Zongker881dd402009-09-20 14:03:55 -0700757 so_far += target_boot.size
758 script.SetProgress(so_far / total_patch_size)
Doug Zongkereef39442009-04-02 12:14:19 -0700759 print "boot image changed; including."
760 else:
761 print "boot image unchanged; skipping."
762
763 if updating_recovery:
Doug Zongker73ef8252009-07-23 15:12:53 -0700764 # Is it better to generate recovery as a patch from the current
765 # boot image, or from the previous recovery image? For large
766 # updates with significant kernel changes, probably the former.
767 # For small updates where the kernel hasn't changed, almost
768 # certainly the latter. We pick the first option. Future
769 # complicated schemes may let us effectively use both.
770 #
771 # A wacky possibility: as long as there is room in the boot
772 # partition, include the binaries and image files from recovery in
773 # the boot image (though not in the ramdisk) so they can be used
774 # as fodder for constructing the recovery image.
Doug Zongker283e2a12010-03-15 17:52:32 -0700775 MakeRecoveryPatch(output_zip, target_recovery, target_boot)
Doug Zongker42265392010-02-12 10:21:00 -0800776 script.DeleteFiles(["/system/recovery-from-boot.p",
777 "/system/etc/install-recovery.sh"])
Doug Zongker73ef8252009-07-23 15:12:53 -0700778 print "recovery image changed; including as patch from boot."
Doug Zongkereef39442009-04-02 12:14:19 -0700779 else:
780 print "recovery image unchanged; skipping."
781
Doug Zongker881dd402009-09-20 14:03:55 -0700782 script.ShowProgress(0.1, 10)
Doug Zongkereef39442009-04-02 12:14:19 -0700783
784 target_symlinks = CopySystemFiles(target_zip, None)
785
786 target_symlinks_d = dict([(i[1], i[0]) for i in target_symlinks])
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700787 temp_script = script.MakeTemporary()
Doug Zongker283e2a12010-03-15 17:52:32 -0700788 Item.GetMetadata(target_zip)
Doug Zongker73ef8252009-07-23 15:12:53 -0700789 Item.Get("system").SetPermissions(temp_script)
Doug Zongkereef39442009-04-02 12:14:19 -0700790
791 # Note that this call will mess up the tree of Items, so make sure
792 # we're done with it.
793 source_symlinks = CopySystemFiles(source_zip, None)
794 source_symlinks_d = dict([(i[1], i[0]) for i in source_symlinks])
795
796 # Delete all the symlinks in source that aren't in target. This
797 # needs to happen before verbatim files are unpacked, in case a
798 # symlink in the source is replaced by a real file in the target.
799 to_delete = []
800 for dest, link in source_symlinks:
801 if link not in target_symlinks_d:
802 to_delete.append(link)
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700803 script.DeleteFiles(to_delete)
Doug Zongkereef39442009-04-02 12:14:19 -0700804
805 if verbatim_targets:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700806 script.Print("Unpacking new files...")
807 script.UnpackPackageDir("system", "/system")
808
Doug Zongker42265392010-02-12 10:21:00 -0800809 if updating_recovery:
810 script.Print("Unpacking new recovery...")
811 script.UnpackPackageDir("recovery", "/system")
812
Doug Zongker05d3dea2009-06-22 11:32:31 -0700813 script.Print("Symlinks and permissions...")
Doug Zongkereef39442009-04-02 12:14:19 -0700814
815 # Create all the symlinks that don't already exist, or point to
816 # somewhere different than what we want. Delete each symlink before
817 # creating it, since the 'symlink' command won't overwrite.
818 to_create = []
819 for dest, link in target_symlinks:
820 if link in source_symlinks_d:
821 if dest != source_symlinks_d[link]:
822 to_create.append((dest, link))
823 else:
824 to_create.append((dest, link))
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700825 script.DeleteFiles([i[1] for i in to_create])
826 script.MakeSymlinks(to_create)
Doug Zongkereef39442009-04-02 12:14:19 -0700827
828 # Now that the symlinks are created, we can set all the
829 # permissions.
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700830 script.AppendScript(temp_script)
Doug Zongkereef39442009-04-02 12:14:19 -0700831
Doug Zongker881dd402009-09-20 14:03:55 -0700832 # Do device-specific installation (eg, write radio image).
Doug Zongker05d3dea2009-06-22 11:32:31 -0700833 device_specific.IncrementalOTA_InstallEnd()
834
Doug Zongker1c390a22009-05-14 19:06:36 -0700835 if OPTIONS.extra_script is not None:
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700836 scirpt.AppendExtra(OPTIONS.extra_script)
Doug Zongker1c390a22009-05-14 19:06:36 -0700837
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700838 script.AddToZip(target_zip, output_zip)
Doug Zongker2ea21062010-04-28 16:05:21 -0700839 WriteMetadata(metadata, output_zip)
Doug Zongkereef39442009-04-02 12:14:19 -0700840
841
842def main(argv):
843
844 def option_handler(o, a):
845 if o in ("-b", "--board_config"):
Doug Zongkerfdd8e692009-08-03 17:27:48 -0700846 pass # deprecated
Doug Zongkereef39442009-04-02 12:14:19 -0700847 elif o in ("-k", "--package_key"):
848 OPTIONS.package_key = a
Doug Zongkereef39442009-04-02 12:14:19 -0700849 elif o in ("-i", "--incremental_from"):
850 OPTIONS.incremental_source = a
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700851 elif o in ("-w", "--wipe_user_data"):
852 OPTIONS.wipe_user_data = True
Doug Zongker962069c2009-04-23 11:41:58 -0700853 elif o in ("-n", "--no_prereq"):
854 OPTIONS.omit_prereq = True
Doug Zongker1c390a22009-05-14 19:06:36 -0700855 elif o in ("-e", "--extra_script"):
856 OPTIONS.extra_script = a
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700857 elif o in ("-m", "--script_mode"):
858 OPTIONS.script_mode = a
Doug Zongker761e6422009-09-25 10:45:39 -0700859 elif o in ("--worker_threads"):
860 OPTIONS.worker_threads = int(a)
Doug Zongkereef39442009-04-02 12:14:19 -0700861 else:
862 return False
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700863 return True
Doug Zongkereef39442009-04-02 12:14:19 -0700864
865 args = common.ParseOptions(argv, __doc__,
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700866 extra_opts="b:k:i:d:wne:m:",
Doug Zongkereef39442009-04-02 12:14:19 -0700867 extra_long_opts=["board_config=",
868 "package_key=",
Doug Zongkerdbfaae52009-04-21 17:12:54 -0700869 "incremental_from=",
Doug Zongker962069c2009-04-23 11:41:58 -0700870 "wipe_user_data",
Doug Zongker1c390a22009-05-14 19:06:36 -0700871 "no_prereq",
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700872 "extra_script=",
Doug Zongker761e6422009-09-25 10:45:39 -0700873 "script_mode=",
874 "worker_threads="],
Doug Zongkereef39442009-04-02 12:14:19 -0700875 extra_option_handler=option_handler)
876
877 if len(args) != 2:
878 common.Usage(__doc__)
879 sys.exit(1)
880
Doug Zongkerc494d7c2009-06-18 08:43:44 -0700881 if OPTIONS.script_mode not in ("amend", "edify", "auto"):
882 raise ValueError('unknown script mode "%s"' % (OPTIONS.script_mode,))
883
Doug Zongker1c390a22009-05-14 19:06:36 -0700884 if OPTIONS.extra_script is not None:
885 OPTIONS.extra_script = open(OPTIONS.extra_script).read()
886
Doug Zongkereef39442009-04-02 12:14:19 -0700887 print "unzipping target target-files..."
888 OPTIONS.input_tmp = common.UnzipTemp(args[0])
Doug Zongkerfdd8e692009-08-03 17:27:48 -0700889
Doug Zongkerc18736b2009-09-30 09:20:32 -0700890 if OPTIONS.device_specific is None:
891 # look for the device-specific tools extension location in the input
892 try:
893 f = open(os.path.join(OPTIONS.input_tmp, "META", "tool-extensions.txt"))
894 ds = f.read().strip()
895 f.close()
896 if ds:
897 ds = os.path.normpath(ds)
898 print "using device-specific extensions in", ds
899 OPTIONS.device_specific = ds
900 except IOError, e:
901 if e.errno == errno.ENOENT:
902 # nothing specified in the file
903 pass
904 else:
905 raise
906
Doug Zongkerfdd8e692009-08-03 17:27:48 -0700907 common.LoadMaxSizes()
908 if not OPTIONS.max_image_size:
909 print
910 print " WARNING: Failed to load max image sizes; will not enforce"
911 print " image size limits."
912 print
913
Doug Zongkereef39442009-04-02 12:14:19 -0700914 OPTIONS.target_tmp = OPTIONS.input_tmp
915 input_zip = zipfile.ZipFile(args[0], "r")
916 if OPTIONS.package_key:
917 temp_zip_file = tempfile.NamedTemporaryFile()
918 output_zip = zipfile.ZipFile(temp_zip_file, "w",
919 compression=zipfile.ZIP_DEFLATED)
920 else:
921 output_zip = zipfile.ZipFile(args[1], "w",
922 compression=zipfile.ZIP_DEFLATED)
923
924 if OPTIONS.incremental_source is None:
925 WriteFullOTAPackage(input_zip, output_zip)
926 else:
927 print "unzipping source target-files..."
928 OPTIONS.source_tmp = common.UnzipTemp(OPTIONS.incremental_source)
929 source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r")
930 WriteIncrementalOTAPackage(input_zip, source_zip, output_zip)
931
932 output_zip.close()
933 if OPTIONS.package_key:
934 SignOutput(temp_zip_file.name, args[1])
935 temp_zip_file.close()
936
937 common.Cleanup()
938
939 print "done."
940
941
942if __name__ == '__main__':
943 try:
944 main(sys.argv[1:])
945 except common.ExternalError, e:
946 print
947 print " ERROR: %s" % (e,)
948 print
949 sys.exit(1)