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