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 """ 18 Build image output_image_file from input_directory and properties_file. 19 20 Usage: build_image input_directory properties_file output_image_file 21 22 """ 23 import os 24 import os.path 25 import re 26 import subprocess 27 import sys 28 import commands 29 import common 30 import shutil 31 import sparse_img 32 import tempfile 33 34 OPTIONS = common.OPTIONS 35 36 FIXED_SALT = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7" 37 BLOCK_SIZE = 4096 38 39 def 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 53 def 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 61 def 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 70 def 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 80 def 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 95 def GetSimgSize(image_file): 96 simg = sparse_img.SparseImage(image_file, build_map=False) 97 return simg.blocksize * simg.total_blocks 98 99 def 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 105 def 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 141 AdjustPartitionSizeForVerity.results = {} 142 143 def 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 152 def 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 165 def 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 178 def 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 196 def 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 205 def 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 226 def 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 242 def 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 295 def 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 307 def 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 507 def 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 583 def 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 597 def 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 637 if __name__ == '__main__': 638 main(sys.argv[1:]) 639