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