build_image.py 15.2 KB
Newer Older
Ying Wang's avatar
Ying Wang committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#!/usr/bin/env python
#
# Copyright (C) 2011 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Build image output_image_file from input_directory and properties_file.

Usage:  build_image input_directory properties_file output_image_file

"""
import os
Ying Wang's avatar
Ying Wang committed
24
import os.path
Ying Wang's avatar
Ying Wang committed
25 26
import subprocess
import sys
27 28
import commands
import shutil
29
import tempfile
Ying Wang's avatar
Ying Wang committed
30

31 32
FIXED_SALT = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7"

Ying Wang's avatar
Ying Wang committed
33 34 35 36 37 38 39 40 41 42 43 44
def RunCommand(cmd):
  """ Echo and run the given command

  Args:
    cmd: the command represented as a list of strings.
  Returns:
    The exit code.
  """
  print "Running: ", " ".join(cmd)
  p = subprocess.Popen(cmd)
  p.communicate()
  return p.returncode
Ying Wang's avatar
Ying Wang committed
45

46
def GetVerityTreeSize(partition_size):
47
  cmd = "build_verity_tree -s %d"
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
  cmd %= partition_size
  status, output = commands.getstatusoutput(cmd)
  if status:
    print output
    return False, 0
  return True, int(output)

def GetVerityMetadataSize(partition_size):
  cmd = "system/extras/verity/build_verity_metadata.py -s %d"
  cmd %= partition_size
  status, output = commands.getstatusoutput(cmd)
  if status:
    print output
    return False, 0
  return True, int(output)

def AdjustPartitionSizeForVerity(partition_size):
  """Modifies the provided partition size to account for the verity metadata.

  This information is used to size the created image appropriately.
  Args:
    partition_size: the size of the partition to be verified.
  Returns:
    The size of the partition adjusted for verity metadata.
  """
  success, verity_tree_size = GetVerityTreeSize(partition_size)
  if not success:
Dan Albert's avatar
Dan Albert committed
75
    return 0
76 77 78 79 80
  success, verity_metadata_size = GetVerityMetadataSize(partition_size)
  if not success:
    return 0
  return partition_size - verity_tree_size - verity_metadata_size

81
def BuildVerityTree(sparse_image_path, verity_image_path, prop_dict):
Dan Albert's avatar
Dan Albert committed
82 83
  cmd = "build_verity_tree -A %s %s %s" % (
      FIXED_SALT, sparse_image_path, verity_image_path)
84 85 86 87 88 89 90 91 92 93 94 95
  print cmd
  status, output = commands.getstatusoutput(cmd)
  if status:
    print "Could not build verity tree! Error: %s" % output
    return False
  root, salt = output.split()
  prop_dict["verity_root_hash"] = root
  prop_dict["verity_salt"] = salt
  return True

def BuildVerityMetadata(image_size, verity_metadata_path, root_hash, salt,
                        block_device, signer_path, key):
Dan Albert's avatar
Dan Albert committed
96 97 98 99
  cmd_template = (
      "system/extras/verity/build_verity_metadata.py %s %s %s %s %s %s %s")
  cmd = cmd_template % (image_size, verity_metadata_path, root_hash, salt,
                        block_device, signer_path, key)
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
  print cmd
  status, output = commands.getstatusoutput(cmd)
  if status:
    print "Could not build verity metadata! Error: %s" % output
    return False
  return True

def Append2Simg(sparse_image_path, unsparse_image_path, error_message):
  """Appends the unsparse image to the given sparse image.

  Args:
    sparse_image_path: the path to the (sparse) image
    unsparse_image_path: the path to the (unsparse) image
  Returns:
    True on success, False on failure.
  """
  cmd = "append2simg %s %s"
  cmd %= (sparse_image_path, unsparse_image_path)
  print cmd
  status, output = commands.getstatusoutput(cmd)
  if status:
    print "%s: %s" % (error_message, output)
    return False
  return True

Dan Albert's avatar
Dan Albert committed
125 126 127 128
def BuildVerifiedImage(data_image_path, verity_image_path,
                       verity_metadata_path):
  if not Append2Simg(data_image_path, verity_metadata_path,
                     "Could not append verity metadata!"):
129
    return False
Dan Albert's avatar
Dan Albert committed
130 131
  if not Append2Simg(data_image_path, verity_image_path,
                     "Could not append verity tree!"):
132 133 134
    return False
  return True

Geremy Condra's avatar
Geremy Condra committed
135
def UnsparseImage(sparse_image_path, replace=True):
136 137 138 139
  img_dir = os.path.dirname(sparse_image_path)
  unsparse_image_path = "unsparse_" + os.path.basename(sparse_image_path)
  unsparse_image_path = os.path.join(img_dir, unsparse_image_path)
  if os.path.exists(unsparse_image_path):
Geremy Condra's avatar
Geremy Condra committed
140 141 142 143
    if replace:
      os.unlink(unsparse_image_path)
    else:
      return True, unsparse_image_path
144 145 146 147 148 149 150 151 152 153 154 155
  inflate_command = ["simg2img", sparse_image_path, unsparse_image_path]
  exit_code = RunCommand(inflate_command)
  if exit_code != 0:
    os.remove(unsparse_image_path)
    return False, None
  return True, unsparse_image_path

def MakeVerityEnabledImage(out_file, prop_dict):
  """Creates an image that is verifiable using dm-verity.

  Args:
    out_file: the location to write the verifiable image at
Dan Albert's avatar
Dan Albert committed
156 157
    prop_dict: a dictionary of properties required for image creation and
               verification
158 159 160 161 162 163
  Returns:
    True on success, False otherwise.
  """
  # get properties
  image_size = prop_dict["partition_size"]
  block_dev = prop_dict["verity_block_device"]
Paul Lawrence's avatar
Paul Lawrence committed
164
  signer_key = prop_dict["verity_key"] + ".pk8"
165 166 167
  signer_path = prop_dict["verity_signer_cmd"]

  # make a tempdir
168
  tempdir_name = tempfile.mkdtemp(suffix="_verity_images")
169 170 171 172 173 174

  # get partial image paths
  verity_image_path = os.path.join(tempdir_name, "verity.img")
  verity_metadata_path = os.path.join(tempdir_name, "verity_metadata.img")

  # build the verity tree and get the root hash and salt
175
  if not BuildVerityTree(out_file, verity_image_path, prop_dict):
176
    shutil.rmtree(tempdir_name, ignore_errors=True)
177 178 179 180 181
    return False

  # build the metadata blocks
  root_hash = prop_dict["verity_root_hash"]
  salt = prop_dict["verity_salt"]
Dan Albert's avatar
Dan Albert committed
182 183
  if not BuildVerityMetadata(image_size, verity_metadata_path, root_hash, salt,
                             block_dev, signer_path, signer_key):
184
    shutil.rmtree(tempdir_name, ignore_errors=True)
185 186 187 188 189 190
    return False

  # build the full verified image
  if not BuildVerifiedImage(out_file,
                            verity_image_path,
                            verity_metadata_path):
191
    shutil.rmtree(tempdir_name, ignore_errors=True)
192 193
    return False

194
  shutil.rmtree(tempdir_name, ignore_errors=True)
195 196
  return True

197
def BuildImage(in_dir, prop_dict, out_file):
Ying Wang's avatar
Ying Wang committed
198 199 200 201 202 203 204 205 206 207
  """Build an image to out_file from in_dir with property prop_dict.

  Args:
    in_dir: path of input directory.
    prop_dict: property dictionary.
    out_file: path of the output image file.

  Returns:
    True iff the image is built successfully.
  """
208 209
  # system_root_image=true: build a system.img that combines the contents of
  # /system and the ramdisk, and can be mounted at the root of the file system.
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
  origin_in = in_dir
  fs_config = prop_dict.get("fs_config")
  if (prop_dict.get("system_root_image") == "true"
      and prop_dict["mount_point"] == "system"):
    in_dir = tempfile.mkdtemp()
    # Change the mount point to "/"
    prop_dict["mount_point"] = "/"
    if fs_config:
      # We need to merge the fs_config files of system and ramdisk.
      fd, merged_fs_config = tempfile.mkstemp(prefix="root_fs_config",
                                              suffix=".txt")
      os.close(fd)
      with open(merged_fs_config, "w") as fw:
        if "ramdisk_fs_config" in prop_dict:
          with open(prop_dict["ramdisk_fs_config"]) as fr:
            fw.writelines(fr.readlines())
        with open(fs_config) as fr:
          fw.writelines(fr.readlines())
      fs_config = merged_fs_config

Ying Wang's avatar
Ying Wang committed
230 231
  build_command = []
  fs_type = prop_dict.get("fs_type", "")
Ying Wang's avatar
Ying Wang committed
232
  run_fsck = False
233

234 235 236 237
  fs_spans_partition = True
  if fs_type.startswith("squash"):
      fs_spans_partition = False

238
  is_verity_partition = "verity_block_device" in prop_dict
239
  verity_supported = prop_dict.get("verity") == "true"
240 241
  # adjust the partition size to make room for the hashes if this is to be verified
  if verity_supported and is_verity_partition and fs_spans_partition:
242 243 244 245 246 247 248
    partition_size = int(prop_dict.get("partition_size"))
    adjusted_size = AdjustPartitionSizeForVerity(partition_size)
    if not adjusted_size:
      return False
    prop_dict["partition_size"] = str(adjusted_size)
    prop_dict["original_partition_size"] = str(partition_size)

Ying Wang's avatar
Ying Wang committed
249 250 251 252
  if fs_type.startswith("ext"):
    build_command = ["mkuserimg.sh"]
    if "extfs_sparse_flag" in prop_dict:
      build_command.append(prop_dict["extfs_sparse_flag"])
Ying Wang's avatar
Ying Wang committed
253
      run_fsck = True
Ying Wang's avatar
Ying Wang committed
254 255
    build_command.extend([in_dir, out_file, fs_type,
                          prop_dict["mount_point"]])
256
    build_command.append(prop_dict["partition_size"])
257 258
    if "journal_size" in prop_dict:
      build_command.extend(["-j", prop_dict["journal_size"]])
259 260
    if "timestamp" in prop_dict:
      build_command.extend(["-T", str(prop_dict["timestamp"])])
261
    if fs_config:
262
      build_command.extend(["-C", fs_config])
263 264
    if "block_list" in prop_dict:
      build_command.extend(["-B", prop_dict["block_list"]])
265
    build_command.extend(["-L", prop_dict["mount_point"]])
266
    if "selinux_fc" in prop_dict:
267
      build_command.append(prop_dict["selinux_fc"])
268 269 270
  elif fs_type.startswith("squash"):
    build_command = ["mksquashfsimage.sh"]
    build_command.extend([in_dir, out_file])
271
    build_command.extend(["-s"])
272
    build_command.extend(["-m", prop_dict["mount_point"]])
273
    if "selinux_fc" in prop_dict:
274
      build_command.extend(["-c", prop_dict["selinux_fc"]])
275 276 277
  elif fs_type.startswith("f2fs"):
    build_command = ["mkf2fsuserimg.sh"]
    build_command.extend([out_file, prop_dict["partition_size"]])
Ying Wang's avatar
Ying Wang committed
278 279 280 281 282 283
  else:
    build_command = ["mkyaffs2image", "-f"]
    if prop_dict.get("mkyaffs2_extra_flags", None):
      build_command.extend(prop_dict["mkyaffs2_extra_flags"].split())
    build_command.append(in_dir)
    build_command.append(out_file)
284 285 286
    if "selinux_fc" in prop_dict:
      build_command.append(prop_dict["selinux_fc"])
      build_command.append(prop_dict["mount_point"])
Ying Wang's avatar
Ying Wang committed
287

288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
  if in_dir != origin_in:
    # Construct a staging directory of the root file system.
    ramdisk_dir = prop_dict.get("ramdisk_dir")
    if ramdisk_dir:
      shutil.rmtree(in_dir)
      shutil.copytree(ramdisk_dir, in_dir, symlinks=True)
    staging_system = os.path.join(in_dir, "system")
    shutil.rmtree(staging_system, ignore_errors=True)
    shutil.copytree(origin_in, staging_system, symlinks=True)
  try:
    exit_code = RunCommand(build_command)
  finally:
    if in_dir != origin_in:
      # Clean up temporary directories and files.
      shutil.rmtree(in_dir, ignore_errors=True)
      if fs_config:
        os.remove(fs_config)
Ying Wang's avatar
Ying Wang committed
305 306 307
  if exit_code != 0:
    return False

308 309 310 311 312 313 314 315 316 317 318 319 320 321
  if not fs_spans_partition:
    mount_point = prop_dict.get("mount_point")
    partition_size = int(prop_dict.get("partition_size"))
    image_size = os.stat(out_file).st_size
    if image_size > partition_size:
        print "Error: %s image size of %d is larger than partition size of %d" % (mount_point, image_size, partition_size)
        return False
    if verity_supported and is_verity_partition:
        if 2 * image_size - AdjustPartitionSizeForVerity(image_size) > partition_size:
            print "Error: No more room on %s to fit verity data" % mount_point
            return False
    prop_dict["original_partition_size"] = prop_dict["partition_size"]
    prop_dict["partition_size"] = str(image_size)

322
  # create the verified image if this is to be verified
323
  if verity_supported and is_verity_partition:
324 325 326
    if not MakeVerityEnabledImage(out_file, prop_dict):
      return False

327
  if run_fsck and prop_dict.get("skip_fsck") != "true":
Geremy Condra's avatar
Geremy Condra committed
328
    success, unsparse_image = UnsparseImage(out_file, replace=False)
329
    if not success:
Ying Wang's avatar
Ying Wang committed
330 331 332 333 334 335 336 337 338
      return False

    # Run e2fsck on the inflated image file
    e2fsck_command = ["e2fsck", "-f", "-n", unsparse_image]
    exit_code = RunCommand(e2fsck_command)

    os.remove(unsparse_image)

  return exit_code == 0
Ying Wang's avatar
Ying Wang committed
339 340 341 342 343 344 345 346 347


def ImagePropFromGlobalDict(glob_dict, mount_point):
  """Build an image property dictionary from the global dictionary.

  Args:
    glob_dict: the global dictionary from the build system.
    mount_point: such as "system", "data" etc.
  """
348 349 350 351 352
  d = {}
  if "build.prop" in glob_dict:
    bp = glob_dict["build.prop"]
    if "ro.build.date.utc" in bp:
      d["timestamp"] = bp["ro.build.date.utc"]
Ying Wang's avatar
Ying Wang committed
353 354 355 356 357

  def copy_prop(src_p, dest_p):
    if src_p in glob_dict:
      d[dest_p] = str(glob_dict[src_p])

Ying Wang's avatar
Ying Wang committed
358 359 360
  common_props = (
      "extfs_sparse_flag",
      "mkyaffs2_extra_flags",
361
      "selinux_fc",
362
      "skip_fsck",
363 364
      "verity",
      "verity_key",
365
      "verity_signer_cmd"
Ying Wang's avatar
Ying Wang committed
366 367
      )
  for p in common_props:
Ying Wang's avatar
Ying Wang committed
368
    copy_prop(p, p)
Ying Wang's avatar
Ying Wang committed
369 370 371

  d["mount_point"] = mount_point
  if mount_point == "system":
Ying Wang's avatar
Ying Wang committed
372
    copy_prop("fs_type", "fs_type")
Dan Albert's avatar
Dan Albert committed
373 374
    # Copy the generic sysetem fs type first, override with specific one if
    # available.
375
    copy_prop("system_fs_type", "fs_type")
Ying Wang's avatar
Ying Wang committed
376
    copy_prop("system_size", "partition_size")
377
    copy_prop("system_journal_size", "journal_size")
378
    copy_prop("system_verity_block_device", "verity_block_device")
379 380
    copy_prop("system_root_image", "system_root_image")
    copy_prop("ramdisk_dir", "ramdisk_dir")
Ying Wang's avatar
Ying Wang committed
381
  elif mount_point == "data":
382
    # Copy the generic fs type first, override with specific one if available.
Ying Wang's avatar
Ying Wang committed
383
    copy_prop("fs_type", "fs_type")
384
    copy_prop("userdata_fs_type", "fs_type")
Ying Wang's avatar
Ying Wang committed
385 386 387 388
    copy_prop("userdata_size", "partition_size")
  elif mount_point == "cache":
    copy_prop("cache_fs_type", "fs_type")
    copy_prop("cache_size", "partition_size")
Ying Wang's avatar
Ying Wang committed
389 390 391
  elif mount_point == "vendor":
    copy_prop("vendor_fs_type", "fs_type")
    copy_prop("vendor_size", "partition_size")
392
    copy_prop("vendor_journal_size", "journal_size")
393
    copy_prop("vendor_verity_block_device", "verity_block_device")
Ying Wang's avatar
Ying Wang committed
394 395 396
  elif mount_point == "oem":
    copy_prop("fs_type", "fs_type")
    copy_prop("oem_size", "partition_size")
397
    copy_prop("oem_journal_size", "journal_size")
Ying Wang's avatar
Ying Wang committed
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425

  return d


def LoadGlobalDict(filename):
  """Load "name=value" pairs from filename"""
  d = {}
  f = open(filename)
  for line in f:
    line = line.strip()
    if not line or line.startswith("#"):
      continue
    k, v = line.split("=", 1)
    d[k] = v
  f.close()
  return d


def main(argv):
  if len(argv) != 3:
    print __doc__
    sys.exit(1)

  in_dir = argv[0]
  glob_dict_file = argv[1]
  out_file = argv[2]

  glob_dict = LoadGlobalDict(glob_dict_file)
426 427 428
  if "mount_point" in glob_dict:
    # The caller knows the mount point and provides a dictionay needed by BuildImage().
    image_properties = glob_dict
Ying Wang's avatar
Ying Wang committed
429
  else:
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
    image_filename = os.path.basename(out_file)
    mount_point = ""
    if image_filename == "system.img":
      mount_point = "system"
    elif image_filename == "userdata.img":
      mount_point = "data"
    elif image_filename == "cache.img":
      mount_point = "cache"
    elif image_filename == "vendor.img":
      mount_point = "vendor"
    elif image_filename == "oem.img":
      mount_point = "oem"
    else:
      print >> sys.stderr, "error: unknown image file name ", image_filename
      exit(1)

    image_properties = ImagePropFromGlobalDict(glob_dict, mount_point)
Ying Wang's avatar
Ying Wang committed
447 448

  if not BuildImage(in_dir, image_properties, out_file):
Dan Albert's avatar
Dan Albert committed
449 450
    print >> sys.stderr, "error: failed to build %s from %s" % (out_file,
                                                                in_dir)
Ying Wang's avatar
Ying Wang committed
451 452 453 454 455
    exit(1)


if __name__ == '__main__':
  main(sys.argv[1:])