Home | History | Annotate | Download | only in releasetools
      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 tempfile
     32 
     33 OPTIONS = common.OPTIONS
     34 
     35 FIXED_SALT = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7"
     36 
     37 def 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 
     51 def 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 
     60 def 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 
     70 def 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 
     87 def 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 
    100 def 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 
    113 def 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 
    131 def 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 
    141 def 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 
    157 def 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 
    207 def 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 
    394 def 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 
    460 def 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 
    474 def 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 
    514 if __name__ == '__main__':
    515   main(sys.argv[1:])
    516