1#!/usr/bin/env python 2# 3# Copyright (C) 2011 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""" 18Build image output_image_file from input_directory and properties_file. 19 20Usage: build_image input_directory properties_file output_image_file 21 22""" 23import os 24import os.path 25import re 26import subprocess 27import sys 28import commands 29import common 30import shutil 31import sparse_img 32import tempfile 33 34OPTIONS = common.OPTIONS 35 36FIXED_SALT = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7" 37BLOCK_SIZE = 4096 38 39def RunCommand(cmd): 40 """Echo and run the given command. 41 42 Args: 43 cmd: the command represented as a list of strings. 44 Returns: 45 A tuple of the output and the exit code. 46 """ 47 print "Running: ", " ".join(cmd) 48 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 49 output, _ = p.communicate() 50 print "%s" % (output.rstrip(),) 51 return (output, p.returncode) 52 53def GetVerityFECSize(partition_size): 54 cmd = "fec -s %d" % partition_size 55 status, output = commands.getstatusoutput(cmd) 56 if status: 57 print output 58 return False, 0 59 return True, int(output) 60 61def GetVerityTreeSize(partition_size): 62 cmd = "build_verity_tree -s %d" 63 cmd %= partition_size 64 status, output = commands.getstatusoutput(cmd) 65 if status: 66 print output 67 return False, 0 68 return True, int(output) 69 70def GetVerityMetadataSize(partition_size): 71 cmd = "system/extras/verity/build_verity_metadata.py -s %d" 72 cmd %= partition_size 73 74 status, output = commands.getstatusoutput(cmd) 75 if status: 76 print output 77 return False, 0 78 return True, int(output) 79 80def GetVeritySize(partition_size, fec_supported): 81 success, verity_tree_size = GetVerityTreeSize(partition_size) 82 if not success: 83 return 0 84 success, verity_metadata_size = GetVerityMetadataSize(partition_size) 85 if not success: 86 return 0 87 verity_size = verity_tree_size + verity_metadata_size 88 if fec_supported: 89 success, fec_size = GetVerityFECSize(partition_size + verity_size) 90 if not success: 91 return 0 92 return verity_size + fec_size 93 return verity_size 94 95def GetSimgSize(image_file): 96 simg = sparse_img.SparseImage(image_file, build_map=False) 97 return simg.blocksize * simg.total_blocks 98 99def ZeroPadSimg(image_file, pad_size): 100 blocks = pad_size // BLOCK_SIZE 101 print("Padding %d blocks (%d bytes)" % (blocks, pad_size)) 102 simg = sparse_img.SparseImage(image_file, mode="r+b", build_map=False) 103 simg.AppendFillChunk(0, blocks) 104 105def AdjustPartitionSizeForVerity(partition_size, fec_supported): 106 """Modifies the provided partition size to account for the verity metadata. 107 108 This information is used to size the created image appropriately. 109 Args: 110 partition_size: the size of the partition to be verified. 111 Returns: 112 The size of the partition adjusted for verity metadata. 113 """ 114 key = "%d %d" % (partition_size, fec_supported) 115 if key in AdjustPartitionSizeForVerity.results: 116 return AdjustPartitionSizeForVerity.results[key] 117 118 hi = partition_size 119 if hi % BLOCK_SIZE != 0: 120 hi = (hi // BLOCK_SIZE) * BLOCK_SIZE 121 122 # verity tree and fec sizes depend on the partition size, which 123 # means this estimate is always going to be unnecessarily small 124 lo = partition_size - GetVeritySize(hi, fec_supported) 125 result = lo 126 127 # do a binary search for the optimal size 128 while lo < hi: 129 i = ((lo + hi) // (2 * BLOCK_SIZE)) * BLOCK_SIZE 130 size = i + GetVeritySize(i, fec_supported) 131 if size <= partition_size: 132 if result < i: 133 result = i 134 lo = i + BLOCK_SIZE 135 else: 136 hi = i 137 138 AdjustPartitionSizeForVerity.results[key] = result 139 return result 140 141AdjustPartitionSizeForVerity.results = {} 142 143def BuildVerityFEC(sparse_image_path, verity_path, verity_fec_path): 144 cmd = "fec -e %s %s %s" % (sparse_image_path, verity_path, verity_fec_path) 145 print cmd 146 status, output = commands.getstatusoutput(cmd) 147 if status: 148 print "Could not build FEC data! Error: %s" % output 149 return False 150 return True 151 152def BuildVerityTree(sparse_image_path, verity_image_path, prop_dict): 153 cmd = "build_verity_tree -A %s %s %s" % ( 154 FIXED_SALT, sparse_image_path, verity_image_path) 155 print cmd 156 status, output = commands.getstatusoutput(cmd) 157 if status: 158 print "Could not build verity tree! Error: %s" % output 159 return False 160 root, salt = output.split() 161 prop_dict["verity_root_hash"] = root 162 prop_dict["verity_salt"] = salt 163 return True 164 165def BuildVerityMetadata(image_size, verity_metadata_path, root_hash, salt, 166 block_device, signer_path, key): 167 cmd_template = ( 168 "system/extras/verity/build_verity_metadata.py %s %s %s %s %s %s %s") 169 cmd = cmd_template % (image_size, verity_metadata_path, root_hash, salt, 170 block_device, signer_path, key) 171 print cmd 172 status, output = commands.getstatusoutput(cmd) 173 if status: 174 print "Could not build verity metadata! Error: %s" % output 175 return False 176 return True 177 178def Append2Simg(sparse_image_path, unsparse_image_path, error_message): 179 """Appends the unsparse image to the given sparse image. 180 181 Args: 182 sparse_image_path: the path to the (sparse) image 183 unsparse_image_path: the path to the (unsparse) image 184 Returns: 185 True on success, False on failure. 186 """ 187 cmd = "append2simg %s %s" 188 cmd %= (sparse_image_path, unsparse_image_path) 189 print cmd 190 status, output = commands.getstatusoutput(cmd) 191 if status: 192 print "%s: %s" % (error_message, output) 193 return False 194 return True 195 196def Append(target, file_to_append, error_message): 197 cmd = 'cat %s >> %s' % (file_to_append, target) 198 print cmd 199 status, output = commands.getstatusoutput(cmd) 200 if status: 201 print "%s: %s" % (error_message, output) 202 return False 203 return True 204 205def BuildVerifiedImage(data_image_path, verity_image_path, 206 verity_metadata_path, verity_fec_path, 207 fec_supported): 208 if not Append(verity_image_path, verity_metadata_path, 209 "Could not append verity metadata!"): 210 return False 211 212 if fec_supported: 213 # build FEC for the entire partition, including metadata 214 if not BuildVerityFEC(data_image_path, verity_image_path, 215 verity_fec_path): 216 return False 217 218 if not Append(verity_image_path, verity_fec_path, "Could not append FEC!"): 219 return False 220 221 if not Append2Simg(data_image_path, verity_image_path, 222 "Could not append verity data!"): 223 return False 224 return True 225 226def UnsparseImage(sparse_image_path, replace=True): 227 img_dir = os.path.dirname(sparse_image_path) 228 unsparse_image_path = "unsparse_" + os.path.basename(sparse_image_path) 229 unsparse_image_path = os.path.join(img_dir, unsparse_image_path) 230 if os.path.exists(unsparse_image_path): 231 if replace: 232 os.unlink(unsparse_image_path) 233 else: 234 return True, unsparse_image_path 235 inflate_command = ["simg2img", sparse_image_path, unsparse_image_path] 236 (_, exit_code) = RunCommand(inflate_command) 237 if exit_code != 0: 238 os.remove(unsparse_image_path) 239 return False, None 240 return True, unsparse_image_path 241 242def MakeVerityEnabledImage(out_file, fec_supported, prop_dict): 243 """Creates an image that is verifiable using dm-verity. 244 245 Args: 246 out_file: the location to write the verifiable image at 247 prop_dict: a dictionary of properties required for image creation and 248 verification 249 Returns: 250 True on success, False otherwise. 251 """ 252 # get properties 253 image_size = prop_dict["partition_size"] 254 block_dev = prop_dict["verity_block_device"] 255 signer_key = prop_dict["verity_key"] + ".pk8" 256 if OPTIONS.verity_signer_path is not None: 257 signer_path = OPTIONS.verity_signer_path + ' ' 258 signer_path += ' '.join(OPTIONS.verity_signer_args) 259 else: 260 signer_path = prop_dict["verity_signer_cmd"] 261 262 # make a tempdir 263 tempdir_name = tempfile.mkdtemp(suffix="_verity_images") 264 265 # get partial image paths 266 verity_image_path = os.path.join(tempdir_name, "verity.img") 267 verity_metadata_path = os.path.join(tempdir_name, "verity_metadata.img") 268 verity_fec_path = os.path.join(tempdir_name, "verity_fec.img") 269 270 # build the verity tree and get the root hash and salt 271 if not BuildVerityTree(out_file, verity_image_path, prop_dict): 272 shutil.rmtree(tempdir_name, ignore_errors=True) 273 return False 274 275 # build the metadata blocks 276 root_hash = prop_dict["verity_root_hash"] 277 salt = prop_dict["verity_salt"] 278 if not BuildVerityMetadata(image_size, verity_metadata_path, root_hash, salt, 279 block_dev, signer_path, signer_key): 280 shutil.rmtree(tempdir_name, ignore_errors=True) 281 return False 282 283 # build the full verified image 284 if not BuildVerifiedImage(out_file, 285 verity_image_path, 286 verity_metadata_path, 287 verity_fec_path, 288 fec_supported): 289 shutil.rmtree(tempdir_name, ignore_errors=True) 290 return False 291 292 shutil.rmtree(tempdir_name, ignore_errors=True) 293 return True 294 295def ConvertBlockMapToBaseFs(block_map_file): 296 fd, base_fs_file = tempfile.mkstemp(prefix="script_gen_", 297 suffix=".base_fs") 298 os.close(fd) 299 300 convert_command = ["blk_alloc_to_base_fs", block_map_file, base_fs_file] 301 (_, exit_code) = RunCommand(convert_command) 302 if exit_code != 0: 303 os.remove(base_fs_file) 304 return None 305 return base_fs_file 306 307def BuildImage(in_dir, prop_dict, out_file, target_out=None): 308 """Build an image to out_file from in_dir with property prop_dict. 309 310 Args: 311 in_dir: path of input directory. 312 prop_dict: property dictionary. 313 out_file: path of the output image file. 314 target_out: path of the product out directory to read device specific FS config files. 315 316 Returns: 317 True iff the image is built successfully. 318 """ 319 # system_root_image=true: build a system.img that combines the contents of 320 # /system and the ramdisk, and can be mounted at the root of the file system. 321 origin_in = in_dir 322 fs_config = prop_dict.get("fs_config") 323 base_fs_file = None 324 if (prop_dict.get("system_root_image") == "true" 325 and prop_dict["mount_point"] == "system"): 326 in_dir = tempfile.mkdtemp() 327 # Change the mount point to "/" 328 prop_dict["mount_point"] = "/" 329 if fs_config: 330 # We need to merge the fs_config files of system and ramdisk. 331 fd, merged_fs_config = tempfile.mkstemp(prefix="root_fs_config", 332 suffix=".txt") 333 os.close(fd) 334 with open(merged_fs_config, "w") as fw: 335 if "ramdisk_fs_config" in prop_dict: 336 with open(prop_dict["ramdisk_fs_config"]) as fr: 337 fw.writelines(fr.readlines()) 338 with open(fs_config) as fr: 339 fw.writelines(fr.readlines()) 340 fs_config = merged_fs_config 341 342 build_command = [] 343 fs_type = prop_dict.get("fs_type", "") 344 run_fsck = False 345 346 fs_spans_partition = True 347 if fs_type.startswith("squash"): 348 fs_spans_partition = False 349 350 is_verity_partition = "verity_block_device" in prop_dict 351 verity_supported = prop_dict.get("verity") == "true" 352 verity_fec_supported = prop_dict.get("verity_fec") == "true" 353 354 # Adjust the partition size to make room for the hashes if this is to be 355 # verified. 356 if verity_supported and is_verity_partition: 357 partition_size = int(prop_dict.get("partition_size")) 358 adjusted_size = AdjustPartitionSizeForVerity(partition_size, 359 verity_fec_supported) 360 if not adjusted_size: 361 return False 362 prop_dict["partition_size"] = str(adjusted_size) 363 prop_dict["original_partition_size"] = str(partition_size) 364 365 if fs_type.startswith("ext"): 366 build_command = ["mkuserimg.sh"] 367 if "extfs_sparse_flag" in prop_dict: 368 build_command.append(prop_dict["extfs_sparse_flag"]) 369 run_fsck = True 370 build_command.extend([in_dir, out_file, fs_type, 371 prop_dict["mount_point"]]) 372 build_command.append(prop_dict["partition_size"]) 373 if "journal_size" in prop_dict: 374 build_command.extend(["-j", prop_dict["journal_size"]]) 375 if "timestamp" in prop_dict: 376 build_command.extend(["-T", str(prop_dict["timestamp"])]) 377 if fs_config: 378 build_command.extend(["-C", fs_config]) 379 if target_out: 380 build_command.extend(["-D", target_out]) 381 if "block_list" in prop_dict: 382 build_command.extend(["-B", prop_dict["block_list"]]) 383 if "base_fs_file" in prop_dict: 384 base_fs_file = ConvertBlockMapToBaseFs(prop_dict["base_fs_file"]) 385 if base_fs_file is None: 386 return False 387 build_command.extend(["-d", base_fs_file]) 388 build_command.extend(["-L", prop_dict["mount_point"]]) 389 if "selinux_fc" in prop_dict: 390 build_command.append(prop_dict["selinux_fc"]) 391 elif fs_type.startswith("squash"): 392 build_command = ["mksquashfsimage.sh"] 393 build_command.extend([in_dir, out_file]) 394 if "squashfs_sparse_flag" in prop_dict: 395 build_command.extend([prop_dict["squashfs_sparse_flag"]]) 396 build_command.extend(["-m", prop_dict["mount_point"]]) 397 if target_out: 398 build_command.extend(["-d", target_out]) 399 if fs_config: 400 build_command.extend(["-C", fs_config]) 401 if "selinux_fc" in prop_dict: 402 build_command.extend(["-c", prop_dict["selinux_fc"]]) 403 if "block_list" in prop_dict: 404 build_command.extend(["-B", prop_dict["block_list"]]) 405 if "squashfs_compressor" in prop_dict: 406 build_command.extend(["-z", prop_dict["squashfs_compressor"]]) 407 if "squashfs_compressor_opt" in prop_dict: 408 build_command.extend(["-zo", prop_dict["squashfs_compressor_opt"]]) 409 if "squashfs_disable_4k_align" in prop_dict and prop_dict.get("squashfs_disable_4k_align") == "true": 410 build_command.extend(["-a"]) 411 elif fs_type.startswith("f2fs"): 412 build_command = ["mkf2fsuserimg.sh"] 413 build_command.extend([out_file, prop_dict["partition_size"]]) 414 else: 415 build_command = ["mkyaffs2image", "-f"] 416 if prop_dict.get("mkyaffs2_extra_flags", None): 417 build_command.extend(prop_dict["mkyaffs2_extra_flags"].split()) 418 build_command.append(in_dir) 419 build_command.append(out_file) 420 if "selinux_fc" in prop_dict: 421 build_command.append(prop_dict["selinux_fc"]) 422 build_command.append(prop_dict["mount_point"]) 423 424 if in_dir != origin_in: 425 # Construct a staging directory of the root file system. 426 ramdisk_dir = prop_dict.get("ramdisk_dir") 427 if ramdisk_dir: 428 shutil.rmtree(in_dir) 429 shutil.copytree(ramdisk_dir, in_dir, symlinks=True) 430 staging_system = os.path.join(in_dir, "system") 431 shutil.rmtree(staging_system, ignore_errors=True) 432 shutil.copytree(origin_in, staging_system, symlinks=True) 433 434 reserved_blocks = prop_dict.get("has_ext4_reserved_blocks") == "true" 435 ext4fs_output = None 436 437 try: 438 if reserved_blocks and fs_type.startswith("ext4"): 439 (ext4fs_output, exit_code) = RunCommand(build_command) 440 else: 441 (_, exit_code) = RunCommand(build_command) 442 finally: 443 if in_dir != origin_in: 444 # Clean up temporary directories and files. 445 shutil.rmtree(in_dir, ignore_errors=True) 446 if fs_config: 447 os.remove(fs_config) 448 if base_fs_file is not None: 449 os.remove(base_fs_file) 450 if exit_code != 0: 451 return False 452 453 # Bug: 21522719, 22023465 454 # There are some reserved blocks on ext4 FS (lesser of 4096 blocks and 2%). 455 # We need to deduct those blocks from the available space, since they are 456 # not writable even with root privilege. It only affects devices using 457 # file-based OTA and a kernel version of 3.10 or greater (currently just 458 # sprout). 459 if reserved_blocks and fs_type.startswith("ext4"): 460 assert ext4fs_output is not None 461 ext4fs_stats = re.compile( 462 r'Created filesystem with .* (?P<used_blocks>[0-9]+)/' 463 r'(?P<total_blocks>[0-9]+) blocks') 464 m = ext4fs_stats.match(ext4fs_output.strip().split('\n')[-1]) 465 used_blocks = int(m.groupdict().get('used_blocks')) 466 total_blocks = int(m.groupdict().get('total_blocks')) 467 reserved_blocks = min(4096, int(total_blocks * 0.02)) 468 adjusted_blocks = total_blocks - reserved_blocks 469 if used_blocks > adjusted_blocks: 470 mount_point = prop_dict.get("mount_point") 471 print("Error: Not enough room on %s (total: %d blocks, used: %d blocks, " 472 "reserved: %d blocks, available: %d blocks)" % ( 473 mount_point, total_blocks, used_blocks, reserved_blocks, 474 adjusted_blocks)) 475 return False 476 477 if not fs_spans_partition: 478 mount_point = prop_dict.get("mount_point") 479 partition_size = int(prop_dict.get("partition_size")) 480 image_size = GetSimgSize(out_file) 481 if image_size > partition_size: 482 print("Error: %s image size of %d is larger than partition size of " 483 "%d" % (mount_point, image_size, partition_size)) 484 return False 485 if verity_supported and is_verity_partition: 486 ZeroPadSimg(out_file, partition_size - image_size) 487 488 # create the verified image if this is to be verified 489 if verity_supported and is_verity_partition: 490 if not MakeVerityEnabledImage(out_file, verity_fec_supported, prop_dict): 491 return False 492 493 if run_fsck and prop_dict.get("skip_fsck") != "true": 494 success, unsparse_image = UnsparseImage(out_file, replace=False) 495 if not success: 496 return False 497 498 # Run e2fsck on the inflated image file 499 e2fsck_command = ["e2fsck", "-f", "-n", unsparse_image] 500 (_, exit_code) = RunCommand(e2fsck_command) 501 502 os.remove(unsparse_image) 503 504 return exit_code == 0 505 506 507def ImagePropFromGlobalDict(glob_dict, mount_point): 508 """Build an image property dictionary from the global dictionary. 509 510 Args: 511 glob_dict: the global dictionary from the build system. 512 mount_point: such as "system", "data" etc. 513 """ 514 d = {} 515 516 if "build.prop" in glob_dict: 517 bp = glob_dict["build.prop"] 518 if "ro.build.date.utc" in bp: 519 d["timestamp"] = bp["ro.build.date.utc"] 520 521 def copy_prop(src_p, dest_p): 522 if src_p in glob_dict: 523 d[dest_p] = str(glob_dict[src_p]) 524 525 common_props = ( 526 "extfs_sparse_flag", 527 "squashfs_sparse_flag", 528 "mkyaffs2_extra_flags", 529 "selinux_fc", 530 "skip_fsck", 531 "verity", 532 "verity_key", 533 "verity_signer_cmd", 534 "verity_fec" 535 ) 536 for p in common_props: 537 copy_prop(p, p) 538 539 d["mount_point"] = mount_point 540 if mount_point == "system": 541 copy_prop("fs_type", "fs_type") 542 # Copy the generic sysetem fs type first, override with specific one if 543 # available. 544 copy_prop("system_fs_type", "fs_type") 545 copy_prop("system_size", "partition_size") 546 copy_prop("system_journal_size", "journal_size") 547 copy_prop("system_verity_block_device", "verity_block_device") 548 copy_prop("system_root_image", "system_root_image") 549 copy_prop("ramdisk_dir", "ramdisk_dir") 550 copy_prop("ramdisk_fs_config", "ramdisk_fs_config") 551 copy_prop("has_ext4_reserved_blocks", "has_ext4_reserved_blocks") 552 copy_prop("system_squashfs_compressor", "squashfs_compressor") 553 copy_prop("system_squashfs_compressor_opt", "squashfs_compressor_opt") 554 copy_prop("system_squashfs_disable_4k_align", "squashfs_disable_4k_align") 555 copy_prop("system_base_fs_file", "base_fs_file") 556 elif mount_point == "data": 557 # Copy the generic fs type first, override with specific one if available. 558 copy_prop("fs_type", "fs_type") 559 copy_prop("userdata_fs_type", "fs_type") 560 copy_prop("userdata_size", "partition_size") 561 elif mount_point == "cache": 562 copy_prop("cache_fs_type", "fs_type") 563 copy_prop("cache_size", "partition_size") 564 elif mount_point == "vendor": 565 copy_prop("vendor_fs_type", "fs_type") 566 copy_prop("vendor_size", "partition_size") 567 copy_prop("vendor_journal_size", "journal_size") 568 copy_prop("vendor_verity_block_device", "verity_block_device") 569 copy_prop("has_ext4_reserved_blocks", "has_ext4_reserved_blocks") 570 copy_prop("vendor_squashfs_compressor", "squashfs_compressor") 571 copy_prop("vendor_squashfs_compressor_opt", "squashfs_compressor_opt") 572 copy_prop("vendor_squashfs_disable_4k_align", "squashfs_disable_4k_align") 573 copy_prop("vendor_base_fs_file", "base_fs_file") 574 elif mount_point == "oem": 575 copy_prop("fs_type", "fs_type") 576 copy_prop("oem_size", "partition_size") 577 copy_prop("oem_journal_size", "journal_size") 578 copy_prop("has_ext4_reserved_blocks", "has_ext4_reserved_blocks") 579 580 return d 581 582 583def LoadGlobalDict(filename): 584 """Load "name=value" pairs from filename""" 585 d = {} 586 f = open(filename) 587 for line in f: 588 line = line.strip() 589 if not line or line.startswith("#"): 590 continue 591 k, v = line.split("=", 1) 592 d[k] = v 593 f.close() 594 return d 595 596 597def main(argv): 598 if len(argv) != 4: 599 print __doc__ 600 sys.exit(1) 601 602 in_dir = argv[0] 603 glob_dict_file = argv[1] 604 out_file = argv[2] 605 target_out = argv[3] 606 607 glob_dict = LoadGlobalDict(glob_dict_file) 608 if "mount_point" in glob_dict: 609 # The caller knows the mount point and provides a dictionay needed by 610 # BuildImage(). 611 image_properties = glob_dict 612 else: 613 image_filename = os.path.basename(out_file) 614 mount_point = "" 615 if image_filename == "system.img": 616 mount_point = "system" 617 elif image_filename == "userdata.img": 618 mount_point = "data" 619 elif image_filename == "cache.img": 620 mount_point = "cache" 621 elif image_filename == "vendor.img": 622 mount_point = "vendor" 623 elif image_filename == "oem.img": 624 mount_point = "oem" 625 else: 626 print >> sys.stderr, "error: unknown image file name ", image_filename 627 exit(1) 628 629 image_properties = ImagePropFromGlobalDict(glob_dict, mount_point) 630 631 if not BuildImage(in_dir, image_properties, out_file, target_out): 632 print >> sys.stderr, "error: failed to build %s from %s" % (out_file, 633 in_dir) 634 exit(1) 635 636 637if __name__ == '__main__': 638 main(sys.argv[1:]) 639