• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
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"""
18Builds output_image from the given input_directory, properties_file,
19and writes the image to target_output_directory.
20
21Usage:  build_image input_directory properties_file output_image \\
22            target_output_directory
23"""
24
25import datetime
26
27import argparse
28import glob
29import logging
30import os
31import os.path
32import re
33import shlex
34import shutil
35import sys
36import uuid
37import tempfile
38
39import common
40import verity_utils
41
42
43logger = logging.getLogger(__name__)
44
45OPTIONS = common.OPTIONS
46BLOCK_SIZE = common.BLOCK_SIZE
47BYTES_IN_MB = 1024 * 1024
48
49# Use a fixed timestamp (01/01/2009 00:00:00 UTC) for files when packaging
50# images. (b/24377993, b/80600931)
51FIXED_FILE_TIMESTAMP = int((
52    datetime.datetime(2009, 1, 1, 0, 0, 0, 0, datetime.UTC) -
53    datetime.datetime.fromtimestamp(0, datetime.UTC)).total_seconds())
54
55
56class BuildImageError(Exception):
57  """An Exception raised during image building."""
58
59  def __init__(self, message):
60    Exception.__init__(self, message)
61
62
63def GetDiskUsage(path):
64  """Returns the number of bytes that "path" occupies on host.
65
66  Args:
67    path: The directory or file to calculate size on.
68
69  Returns:
70    The number of bytes based on a 1K block_size.
71  """
72  cmd = ["du", "-b", "-k", "-s", path]
73  output = common.RunAndCheckOutput(cmd, verbose=False)
74  return int(output.split()[0]) * 1024
75
76
77def GetInodeUsage(path):
78  """Returns the number of inodes that "path" occupies on host.
79
80  Args:
81    path: The directory or file to calculate inode number on.
82
83  Returns:
84    The number of inodes used.
85  """
86  cmd = ["find", path, "-print"]
87  output = common.RunAndCheckOutput(cmd, verbose=False)
88  # increase by > 6% as number of files and directories is not whole picture.
89  inodes = output.count('\n')
90  spare_inodes = inodes * 6 // 100
91  min_spare_inodes = 12
92  if spare_inodes < min_spare_inodes:
93    spare_inodes = min_spare_inodes
94  return inodes + spare_inodes
95
96
97def GetFilesystemCharacteristics(fs_type, image_path, sparse_image=True):
98  """Returns various filesystem characteristics of "image_path".
99
100  Args:
101    image_path: The file to analyze.
102    sparse_image: Image is sparse
103
104  Returns:
105    The characteristics dictionary.
106  """
107  unsparse_image_path = image_path
108  if sparse_image:
109    unsparse_image_path = UnsparseImage(image_path, replace=False)
110
111  if fs_type.startswith("ext"):
112    cmd = ["tune2fs", "-l", unsparse_image_path]
113  elif fs_type.startswith("f2fs"):
114    cmd = ["fsck.f2fs", "-l", unsparse_image_path]
115
116  try:
117    output = common.RunAndCheckOutput(cmd, verbose=False)
118  finally:
119    if sparse_image:
120      os.remove(unsparse_image_path)
121  fs_dict = {}
122  for line in output.splitlines():
123    fields = line.split(":")
124    if len(fields) == 2:
125      fs_dict[fields[0].strip()] = fields[1].strip()
126  return fs_dict
127
128
129def UnsparseImage(sparse_image_path, replace=True):
130  img_dir = os.path.dirname(sparse_image_path)
131  unsparse_image_path = "unsparse_" + os.path.basename(sparse_image_path)
132  unsparse_image_path = os.path.join(img_dir, unsparse_image_path)
133  if os.path.exists(unsparse_image_path):
134    if replace:
135      os.unlink(unsparse_image_path)
136    else:
137      return unsparse_image_path
138  inflate_command = ["simg2img", sparse_image_path, unsparse_image_path]
139  try:
140    common.RunAndCheckOutput(inflate_command)
141  except:
142    os.remove(unsparse_image_path)
143    raise
144  return unsparse_image_path
145
146
147def ConvertBlockMapToBaseFs(block_map_file):
148  base_fs_file = common.MakeTempFile(prefix="script_gen_", suffix=".base_fs")
149  convert_command = ["blk_alloc_to_base_fs", block_map_file, base_fs_file]
150  common.RunAndCheckOutput(convert_command)
151  return base_fs_file
152
153
154def SetUpInDirAndFsConfig(origin_in, prop_dict):
155  """Returns the in_dir and fs_config that should be used for image building.
156
157  When building system.img for all targets, it creates and returns a staged dir
158  that combines the contents of /system (i.e. in the given in_dir) and root.
159
160  Args:
161    origin_in: Path to the input directory.
162    prop_dict: A property dict that contains info like partition size. Values
163        may be updated.
164
165  Returns:
166    A tuple of in_dir and fs_config that should be used to build the image.
167  """
168  fs_config = prop_dict.get("fs_config")
169
170  if prop_dict["mount_point"] == "system_other":
171    prop_dict["mount_point"] = "system"
172    return origin_in, fs_config
173
174  if prop_dict["mount_point"] != "system":
175    return origin_in, fs_config
176
177  if "first_pass" in prop_dict:
178    prop_dict["mount_point"] = "/"
179    return prop_dict["first_pass"]
180
181  # Construct a staging directory of the root file system.
182  in_dir = common.MakeTempDir()
183  root_dir = prop_dict.get("root_dir")
184  if root_dir:
185    shutil.rmtree(in_dir)
186    shutil.copytree(root_dir, in_dir, symlinks=True)
187  in_dir_system = os.path.join(in_dir, "system")
188  shutil.rmtree(in_dir_system, ignore_errors=True)
189  shutil.copytree(origin_in, in_dir_system, symlinks=True)
190
191  # Change the mount point to "/".
192  prop_dict["mount_point"] = "/"
193  if fs_config:
194    # We need to merge the fs_config files of system and root.
195    merged_fs_config = common.MakeTempFile(
196        prefix="merged_fs_config", suffix=".txt")
197    with open(merged_fs_config, "w") as fw:
198      if "root_fs_config" in prop_dict:
199        with open(prop_dict["root_fs_config"]) as fr:
200          fw.writelines(fr.readlines())
201      with open(fs_config) as fr:
202        fw.writelines(fr.readlines())
203    fs_config = merged_fs_config
204  prop_dict["first_pass"] = (in_dir, fs_config)
205  return in_dir, fs_config
206
207
208def CheckHeadroom(ext4fs_output, prop_dict):
209  """Checks if there's enough headroom space available.
210
211  Headroom is the reserved space on system image (via PRODUCT_SYSTEM_HEADROOM),
212  which is useful for devices with low disk space that have system image
213  variation between builds. The 'partition_headroom' in prop_dict is the size
214  in bytes, while the numbers in 'ext4fs_output' are for 4K-blocks.
215
216  Args:
217    ext4fs_output: The output string from mke2fs command.
218    prop_dict: The property dict.
219
220  Raises:
221    AssertionError: On invalid input.
222    BuildImageError: On check failure.
223  """
224  assert ext4fs_output is not None
225  assert prop_dict.get('fs_type', '').startswith('ext4')
226  assert 'partition_headroom' in prop_dict
227  assert 'mount_point' in prop_dict
228
229  ext4fs_stats = re.compile(
230      r'Created filesystem with .* (?P<used_blocks>[0-9]+)/'
231      r'(?P<total_blocks>[0-9]+) blocks')
232  last_line = ext4fs_output.strip().split('\n')[-1]
233  m = ext4fs_stats.match(last_line)
234  used_blocks = int(m.groupdict().get('used_blocks'))
235  total_blocks = int(m.groupdict().get('total_blocks'))
236  headroom_blocks = int(prop_dict['partition_headroom']) // BLOCK_SIZE
237  adjusted_blocks = total_blocks - headroom_blocks
238  if used_blocks > adjusted_blocks:
239    mount_point = prop_dict["mount_point"]
240    raise BuildImageError(
241        "Error: Not enough room on {} (total: {} blocks, used: {} blocks, "
242        "headroom: {} blocks, available: {} blocks)".format(
243            mount_point, total_blocks, used_blocks, headroom_blocks,
244            adjusted_blocks))
245
246
247def CalculateSizeAndReserved(prop_dict, size):
248  fs_type = prop_dict.get("fs_type", "")
249  partition_headroom = int(prop_dict.get("partition_headroom", 0))
250  # If not specified, give us 16MB margin for GetDiskUsage error ...
251  reserved_size = int(prop_dict.get(
252      "partition_reserved_size", BYTES_IN_MB * 16))
253
254  if fs_type == "erofs":
255    reserved_size = int(prop_dict.get("partition_reserved_size", 0))
256    if reserved_size == 0:
257      # give .3% margin or a minimum size for AVB footer
258      return max(size * 1003 // 1000, 256 * 1024)
259
260  if fs_type.startswith("ext4") and partition_headroom > reserved_size:
261    reserved_size = partition_headroom
262
263  return int(size * 1.1) + reserved_size
264
265
266def BuildImageMkfs(in_dir, prop_dict, out_file, target_out, fs_config):
267  """Builds a pure image for the files under in_dir and writes it to out_file.
268
269  Args:
270    in_dir: Path to input directory.
271    prop_dict: A property dict that contains info like partition size. Values
272        will be updated with computed values.
273    out_file: The output image file.
274    target_out: Path to the TARGET_OUT directory as in Makefile. It actually
275        points to the /system directory under PRODUCT_OUT. fs_config (the one
276        under system/core/libcutils) reads device specific FS config files from
277        there.
278    fs_config: The fs_config file that drives the prototype
279
280  Raises:
281    BuildImageError: On build image failures.
282  """
283  build_command = []
284  fs_type = prop_dict.get("fs_type", "")
285  run_fsck = None
286  needs_projid = prop_dict.get("needs_projid", 0)
287  needs_casefold = prop_dict.get("needs_casefold", 0)
288  needs_compress = prop_dict.get("needs_compress", 0)
289
290  disable_sparse = "disable_sparse" in prop_dict
291  manual_sparse = False
292
293  if fs_type.startswith("ext"):
294    build_command = [prop_dict["ext_mkuserimg"]]
295    if "extfs_sparse_flag" in prop_dict and not disable_sparse:
296      build_command.append(prop_dict["extfs_sparse_flag"])
297      run_fsck = RunE2fsck
298    build_command.extend([in_dir, out_file, fs_type,
299                          prop_dict["mount_point"]])
300    build_command.append(prop_dict["image_size"])
301    if "journal_size" in prop_dict:
302      build_command.extend(["-j", prop_dict["journal_size"]])
303    if "timestamp" in prop_dict:
304      build_command.extend(["-T", str(prop_dict["timestamp"])])
305    if fs_config:
306      build_command.extend(["-C", fs_config])
307    if target_out:
308      build_command.extend(["-D", target_out])
309    if "block_list" in prop_dict:
310      build_command.extend(["-B", prop_dict["block_list"]])
311    if "base_fs_file" in prop_dict:
312      base_fs_file = ConvertBlockMapToBaseFs(prop_dict["base_fs_file"])
313      build_command.extend(["-d", base_fs_file])
314    build_command.extend(["-L", prop_dict["mount_point"]])
315    if "extfs_inode_count" in prop_dict:
316      build_command.extend(["-i", prop_dict["extfs_inode_count"]])
317    if "extfs_rsv_pct" in prop_dict:
318      build_command.extend(["-M", prop_dict["extfs_rsv_pct"]])
319    if "flash_erase_block_size" in prop_dict:
320      build_command.extend(["-e", prop_dict["flash_erase_block_size"]])
321    if "flash_logical_block_size" in prop_dict:
322      build_command.extend(["-o", prop_dict["flash_logical_block_size"]])
323    # Specify UUID and hash_seed if using mke2fs.
324    if os.path.basename(prop_dict["ext_mkuserimg"]) == "mkuserimg_mke2fs":
325      if "uuid" in prop_dict:
326        build_command.extend(["-U", prop_dict["uuid"]])
327      if "hash_seed" in prop_dict:
328        build_command.extend(["-S", prop_dict["hash_seed"]])
329    if prop_dict.get("ext4_share_dup_blocks") == "true":
330      build_command.append("-c")
331    if (needs_projid):
332      build_command.extend(["--inode_size", "512"])
333    else:
334      build_command.extend(["--inode_size", "256"])
335    if "selinux_fc" in prop_dict:
336      build_command.append(prop_dict["selinux_fc"])
337  elif fs_type.startswith("erofs"):
338    build_command = ["mkfs.erofs"]
339
340    compressor = None
341    if "erofs_default_compressor" in prop_dict:
342      compressor = prop_dict["erofs_default_compressor"]
343    if "erofs_compressor" in prop_dict:
344      compressor = prop_dict["erofs_compressor"]
345    if compressor and compressor != "none":
346      build_command.extend(["-z", compressor])
347
348    compress_hints = None
349    if "erofs_default_compress_hints" in prop_dict:
350      compress_hints = prop_dict["erofs_default_compress_hints"]
351    if "erofs_compress_hints" in prop_dict:
352      compress_hints = prop_dict["erofs_compress_hints"]
353    if compress_hints:
354      build_command.extend(["--compress-hints", compress_hints])
355
356    build_command.extend(["-b", prop_dict.get("erofs_blocksize", "4096")])
357
358    build_command.extend(["--mount-point", prop_dict["mount_point"]])
359    if target_out:
360      build_command.extend(["--product-out", target_out])
361    if fs_config:
362      build_command.extend(["--fs-config-file", fs_config])
363    if "selinux_fc" in prop_dict:
364      build_command.extend(["--file-contexts", prop_dict["selinux_fc"]])
365    if "timestamp" in prop_dict:
366      build_command.extend(["-T", str(prop_dict["timestamp"])])
367    if "uuid" in prop_dict:
368      build_command.extend(["-U", prop_dict["uuid"]])
369    if "block_list" in prop_dict:
370      build_command.extend(["--block-list-file", prop_dict["block_list"]])
371    if "erofs_pcluster_size" in prop_dict:
372      build_command.extend(["-C", prop_dict["erofs_pcluster_size"]])
373    if "erofs_share_dup_blocks" in prop_dict:
374      build_command.extend(["--chunksize", "4096"])
375    if "erofs_use_legacy_compression" in prop_dict:
376      build_command.extend(["-E", "legacy-compress"])
377
378    build_command.extend([out_file, in_dir])
379    if "erofs_sparse_flag" in prop_dict and not disable_sparse:
380      manual_sparse = True
381
382    run_fsck = RunErofsFsck
383  elif fs_type.startswith("squash"):
384    build_command = ["mksquashfsimage"]
385    build_command.extend([in_dir, out_file])
386    if "squashfs_sparse_flag" in prop_dict and not disable_sparse:
387      build_command.extend([prop_dict["squashfs_sparse_flag"]])
388    build_command.extend(["-m", prop_dict["mount_point"]])
389    if target_out:
390      build_command.extend(["-d", target_out])
391    if fs_config:
392      build_command.extend(["-C", fs_config])
393    if "selinux_fc" in prop_dict:
394      build_command.extend(["-c", prop_dict["selinux_fc"]])
395    if "block_list" in prop_dict:
396      build_command.extend(["-B", prop_dict["block_list"]])
397    if "squashfs_block_size" in prop_dict:
398      build_command.extend(["-b", prop_dict["squashfs_block_size"]])
399    if "squashfs_compressor" in prop_dict:
400      build_command.extend(["-z", prop_dict["squashfs_compressor"]])
401    if "squashfs_compressor_opt" in prop_dict:
402      build_command.extend(["-zo", prop_dict["squashfs_compressor_opt"]])
403    if prop_dict.get("squashfs_disable_4k_align") == "true":
404      build_command.extend(["-a"])
405  elif fs_type.startswith("f2fs"):
406    build_command = ["mkf2fsuserimg"]
407    build_command.extend([out_file, prop_dict["image_size"]])
408    if "f2fs_sparse_flag" in prop_dict and not disable_sparse:
409      build_command.extend([prop_dict["f2fs_sparse_flag"]])
410    if fs_config:
411      build_command.extend(["-C", fs_config])
412    build_command.extend(["-f", in_dir])
413    if target_out:
414      build_command.extend(["-D", target_out])
415    if "selinux_fc" in prop_dict:
416      build_command.extend(["-s", prop_dict["selinux_fc"]])
417    build_command.extend(["-t", prop_dict["mount_point"]])
418    if "timestamp" in prop_dict:
419      build_command.extend(["-T", str(prop_dict["timestamp"])])
420    if "block_list" in prop_dict:
421      build_command.extend(["-B", prop_dict["block_list"]])
422    build_command.extend(["-L", prop_dict["mount_point"]])
423    if (needs_projid):
424      build_command.append("--prjquota")
425    if (needs_casefold):
426      build_command.append("--casefold")
427    if (needs_compress or prop_dict.get("f2fs_compress") == "true"):
428      build_command.append("--compression")
429    if "ro_mount_point" in prop_dict:
430      build_command.append("--readonly")
431    if (prop_dict.get("f2fs_compress") == "true"):
432      build_command.append("--sldc")
433      if (prop_dict.get("f2fs_sldc_flags") == None):
434        build_command.append(str(0))
435      else:
436        sldc_flags_str = prop_dict.get("f2fs_sldc_flags")
437        sldc_flags = sldc_flags_str.split()
438        build_command.append(str(len(sldc_flags)))
439        build_command.extend(sldc_flags)
440    f2fs_blocksize = prop_dict.get("f2fs_blocksize", "4096")
441    build_command.extend(["-b", f2fs_blocksize])
442  else:
443    raise BuildImageError(
444        "Error: unknown filesystem type: {}".format(fs_type))
445
446  try:
447    mkfs_output = common.RunAndCheckOutput(build_command)
448  except:
449    try:
450      du = GetDiskUsage(in_dir)
451      du_str = "{} bytes ({} MB)".format(du, du // BYTES_IN_MB)
452    # Suppress any errors from GetDiskUsage() to avoid hiding the real errors
453    # from common.RunAndCheckOutput().
454    except Exception:  # pylint: disable=broad-except
455      logger.exception("Failed to compute disk usage with du")
456      du_str = "unknown"
457    print(
458        "Out of space? Out of inodes? The tree size of {} is {}, "
459        "with reserved space of {} bytes ({} MB).".format(
460            in_dir, du_str,
461            int(prop_dict.get("partition_reserved_size", 0)),
462            int(prop_dict.get("partition_reserved_size", 0)) // BYTES_IN_MB))
463    if ("image_size" in prop_dict and "partition_size" in prop_dict):
464      print(
465          "The max image size for filesystem files is {} bytes ({} MB), "
466          "out of a total partition size of {} bytes ({} MB).".format(
467              int(prop_dict["image_size"]),
468              int(prop_dict["image_size"]) // BYTES_IN_MB,
469              int(prop_dict["partition_size"]),
470              int(prop_dict["partition_size"]) // BYTES_IN_MB))
471    raise
472
473  if run_fsck and prop_dict.get("skip_fsck") != "true":
474    run_fsck(out_file)
475
476  if manual_sparse:
477    temp_file = out_file + ".sparse"
478    img2simg_argv = ["img2simg", out_file, temp_file]
479    common.RunAndCheckOutput(img2simg_argv)
480    os.rename(temp_file, out_file)
481
482  return mkfs_output
483
484
485def RunE2fsck(out_file):
486  unsparse_image = UnsparseImage(out_file, replace=False)
487
488  # Run e2fsck on the inflated image file
489  e2fsck_command = ["e2fsck", "-f", "-n", unsparse_image]
490  try:
491    common.RunAndCheckOutput(e2fsck_command)
492  finally:
493    os.remove(unsparse_image)
494
495
496def RunErofsFsck(out_file):
497  fsck_command = ["fsck.erofs", "--extract", out_file]
498  try:
499    common.RunAndCheckOutput(fsck_command)
500  except:
501    print("Check failed for EROFS image {}".format(out_file))
502    raise
503
504
505def SetUUIDIfNotExist(image_props):
506
507  # Use repeatable ext4 FS UUID and hash_seed UUID (based on partition name and
508  # build fingerprint). Also use the legacy build id, because the vbmeta digest
509  # isn't available at this point.
510  what = image_props["mount_point"]
511  fingerprint = image_props.get("fingerprint", "")
512  uuid_seed = what + "-" + fingerprint
513  logger.info("Using fingerprint %s for partition %s", fingerprint, what)
514  image_props["uuid"] = str(uuid.uuid5(uuid.NAMESPACE_URL, uuid_seed))
515  hash_seed = "hash_seed-" + uuid_seed
516  image_props["hash_seed"] = str(uuid.uuid5(uuid.NAMESPACE_URL, hash_seed))
517
518
519def BuildImage(in_dir, prop_dict, out_file, target_out=None):
520  """Builds an image for the files under in_dir and writes it to out_file.
521
522  Args:
523    in_dir: Path to input directory.
524    prop_dict: A property dict that contains info like partition size. Values
525        will be updated with computed values.
526    out_file: The output image file.
527    target_out: Path to the TARGET_OUT directory as in Makefile. It actually
528        points to the /system directory under PRODUCT_OUT. fs_config (the one
529        under system/core/libcutils) reads device specific FS config files from
530        there.
531
532  Raises:
533    BuildImageError: On build image failures.
534  """
535  in_dir, fs_config = SetUpInDirAndFsConfig(in_dir, prop_dict)
536  SetUUIDIfNotExist(prop_dict)
537
538  build_command = []
539  fs_type = prop_dict.get("fs_type", "")
540
541  fs_spans_partition = True
542  if fs_type.startswith("squash") or fs_type.startswith("erofs"):
543    fs_spans_partition = False
544  elif fs_type.startswith("f2fs") and prop_dict.get("f2fs_compress") == "true":
545    fs_spans_partition = False
546
547  # Get a builder for creating an image that's to be verified by Verified Boot,
548  # or None if not applicable.
549  verity_image_builder = verity_utils.CreateVerityImageBuilder(prop_dict)
550
551  disable_sparse = "disable_sparse" in prop_dict
552  mkfs_output = None
553  if (prop_dict.get("use_dynamic_partition_size") == "true" and
554          "partition_size" not in prop_dict):
555    # If partition_size is not defined, use output of `du' + reserved_size.
556    # For compressed file system, it's better to use the compressed size to avoid wasting space.
557    if fs_type.startswith("erofs"):
558      mkfs_output = BuildImageMkfs(
559          in_dir, prop_dict, out_file, target_out, fs_config)
560      if "erofs_sparse_flag" in prop_dict and not disable_sparse:
561        image_path = UnsparseImage(out_file, replace=False)
562        size = GetDiskUsage(image_path)
563        os.remove(image_path)
564      else:
565        size = GetDiskUsage(out_file)
566    else:
567      size = GetDiskUsage(in_dir)
568    logger.info(
569        "The tree size of %s is %d MB.", in_dir, size // BYTES_IN_MB)
570    size = CalculateSizeAndReserved(prop_dict, size)
571    # Round this up to a multiple of 4K so that avbtool works
572    size = common.RoundUpTo4K(size)
573    if fs_type.startswith("ext"):
574      prop_dict["partition_size"] = str(size)
575      prop_dict["image_size"] = str(size)
576      if "extfs_inode_count" not in prop_dict:
577        prop_dict["extfs_inode_count"] = str(GetInodeUsage(in_dir))
578      logger.info(
579          "First Pass based on estimates of %d MB and %s inodes.",
580          size // BYTES_IN_MB, prop_dict["extfs_inode_count"])
581      BuildImageMkfs(in_dir, prop_dict, out_file, target_out, fs_config)
582      sparse_image = False
583      if "extfs_sparse_flag" in prop_dict and not disable_sparse:
584        sparse_image = True
585      fs_dict = GetFilesystemCharacteristics(fs_type, out_file, sparse_image)
586      os.remove(out_file)
587      block_size = int(fs_dict.get("Block size", "4096"))
588      free_size = int(fs_dict.get("Free blocks", "0")) * block_size
589      reserved_size = int(prop_dict.get("partition_reserved_size", 0))
590      partition_headroom = int(fs_dict.get("partition_headroom", 0))
591      if fs_type.startswith("ext4") and partition_headroom > reserved_size:
592        reserved_size = partition_headroom
593      if free_size <= reserved_size:
594        logger.info(
595            "Not worth reducing image %d <= %d.", free_size, reserved_size)
596      else:
597        size -= free_size
598        size += reserved_size
599        if reserved_size == 0:
600          # add .3% margin
601          size = size * 1003 // 1000
602        # Use a minimum size, otherwise we will fail to calculate an AVB footer
603        # or fail to construct an ext4 image.
604        size = max(size, 256 * 1024)
605        if block_size <= 4096:
606          size = common.RoundUpTo4K(size)
607        else:
608          size = ((size + block_size - 1) // block_size) * block_size
609      extfs_inode_count = prop_dict["extfs_inode_count"]
610      inodes = int(fs_dict.get("Inode count", extfs_inode_count))
611      inodes -= int(fs_dict.get("Free inodes", "0"))
612      # add .2% margin or 1 inode, whichever is greater
613      spare_inodes = inodes * 2 // 1000
614      min_spare_inodes = 1
615      if spare_inodes < min_spare_inodes:
616        spare_inodes = min_spare_inodes
617      inodes += spare_inodes
618      prop_dict["extfs_inode_count"] = str(inodes)
619      prop_dict["partition_size"] = str(size)
620      logger.info(
621          "Allocating %d Inodes for %s.", inodes, out_file)
622    elif fs_type.startswith("f2fs") and prop_dict.get("f2fs_compress") == "true":
623      prop_dict["partition_size"] = str(size)
624      prop_dict["image_size"] = str(size)
625      BuildImageMkfs(in_dir, prop_dict, out_file, target_out, fs_config)
626      sparse_image = False
627      if "f2fs_sparse_flag" in prop_dict and not disable_sparse:
628        sparse_image = True
629      fs_dict = GetFilesystemCharacteristics(fs_type, out_file, sparse_image)
630      os.remove(out_file)
631      block_count = int(fs_dict.get("block_count", "0"))
632      log_blocksize = int(fs_dict.get("log_blocksize", "12"))
633      size = block_count << log_blocksize
634      prop_dict["partition_size"] = str(size)
635    if verity_image_builder:
636      size = verity_image_builder.CalculateDynamicPartitionSize(size)
637    prop_dict["partition_size"] = str(size)
638    logger.info(
639        "Allocating %d MB for %s", size // BYTES_IN_MB, out_file)
640
641  prop_dict["image_size"] = prop_dict["partition_size"]
642
643  # Adjust the image size to make room for the hashes if this is to be verified.
644  if verity_image_builder:
645    max_image_size = verity_image_builder.CalculateMaxImageSize()
646    prop_dict["image_size"] = str(max_image_size)
647
648  if not mkfs_output:
649    mkfs_output = BuildImageMkfs(
650        in_dir, prop_dict, out_file, target_out, fs_config)
651
652  # Update the image (eg filesystem size). This can be different eg if mkfs
653  # rounds the requested size down due to alignment.
654  prop_dict["image_size"] = common.sparse_img.GetImagePartitionSize(out_file)
655
656  # Check if there's enough headroom space available for ext4 image.
657  if "partition_headroom" in prop_dict and fs_type.startswith("ext4"):
658    CheckHeadroom(mkfs_output, prop_dict)
659
660  if not fs_spans_partition and verity_image_builder:
661    verity_image_builder.PadSparseImage(out_file)
662
663  # Create the verified image if this is to be verified.
664  if verity_image_builder:
665    verity_image_builder.Build(out_file)
666
667
668def TryParseFingerprint(glob_dict: dict):
669  for (key, val) in glob_dict.items():
670    if not key.endswith("_add_hashtree_footer_args") and not key.endswith("_add_hash_footer_args"):
671      continue
672    for arg in shlex.split(val):
673      m = re.match(r"^com\.android\.build\.\w+\.fingerprint:", arg)
674      if m is None:
675        continue
676      fingerprint = arg[len(m.group()):]
677      glob_dict["fingerprint"] = fingerprint
678      return
679
680def TryParseFingerprintAndTimestamp(glob_dict):
681  """Helper function that parses fingerprint and timestamp from the global dictionary.
682
683  Args:
684    glob_dict: the global dictionary from the build system.
685  """
686  TryParseFingerprint(glob_dict)
687
688  # Set fixed timestamp for building the OTA package.
689  if "use_fixed_timestamp" in glob_dict:
690    glob_dict["timestamp"] = FIXED_FILE_TIMESTAMP
691  if "build.prop" in glob_dict:
692    timestamp = glob_dict["build.prop"].GetProp("ro.build.date.utc")
693    if timestamp:
694      glob_dict["timestamp"] = timestamp
695
696def ImagePropFromGlobalDict(glob_dict, mount_point):
697  """Build an image property dictionary from the global dictionary.
698
699  Args:
700    glob_dict: the global dictionary from the build system.
701    mount_point: such as "system", "data" etc.
702  """
703  d = {}
704  TryParseFingerprintAndTimestamp(glob_dict)
705
706  def copy_prop(src_p, dest_p):
707    """Copy a property from the global dictionary.
708
709    Args:
710      src_p: The source property in the global dictionary.
711      dest_p: The destination property.
712    Returns:
713      True if property was found and copied, False otherwise.
714    """
715    if src_p in glob_dict:
716      d[dest_p] = str(glob_dict[src_p])
717      return True
718    return False
719
720  common_props = (
721      "extfs_sparse_flag",
722      "erofs_default_compressor",
723      "erofs_default_compress_hints",
724      "erofs_pcluster_size",
725      "erofs_blocksize",
726      "erofs_share_dup_blocks",
727      "erofs_sparse_flag",
728      "erofs_use_legacy_compression",
729      "squashfs_sparse_flag",
730      "system_f2fs_compress",
731      "system_f2fs_sldc_flags",
732      "f2fs_sparse_flag",
733      "f2fs_blocksize",
734      "skip_fsck",
735      "ext_mkuserimg",
736      "avb_enable",
737      "avb_avbtool",
738      "use_dynamic_partition_size",
739      "fingerprint",
740      "timestamp",
741  )
742  for p in common_props:
743    copy_prop(p, p)
744
745  ro_mount_points = set([
746      "odm",
747      "odm_dlkm",
748      "oem",
749      "product",
750      "system",
751      "system_dlkm",
752      "system_ext",
753      "system_other",
754      "vendor",
755      "vendor_dlkm",
756  ])
757
758  # Tuple layout: (readonly, specific prop, general prop)
759  fmt_props = (
760      # Generic first, then specific file type.
761      (False, "fs_type", "fs_type"),
762      (False, "{}_fs_type", "fs_type"),
763
764      # Ordering for these doesn't matter.
765      (False, "{}_selinux_fc", "selinux_fc"),
766      (False, "{}_size", "partition_size"),
767      (True, "avb_{}_add_hashtree_footer_args", "avb_add_hashtree_footer_args"),
768      (True, "avb_{}_algorithm", "avb_algorithm"),
769      (True, "avb_{}_hashtree_enable", "avb_hashtree_enable"),
770      (True, "avb_{}_key_path", "avb_key_path"),
771      (True, "avb_{}_salt", "avb_salt"),
772      (True, "erofs_use_legacy_compression", "erofs_use_legacy_compression"),
773      (True, "ext4_share_dup_blocks", "ext4_share_dup_blocks"),
774      (True, "{}_base_fs_file", "base_fs_file"),
775      (True, "{}_disable_sparse", "disable_sparse"),
776      (True, "{}_erofs_compressor", "erofs_compressor"),
777      (True, "{}_erofs_compress_hints", "erofs_compress_hints"),
778      (True, "{}_erofs_pcluster_size", "erofs_pcluster_size"),
779      (True, "{}_erofs_blocksize", "erofs_blocksize"),
780      (True, "{}_erofs_share_dup_blocks", "erofs_share_dup_blocks"),
781      (True, "{}_extfs_inode_count", "extfs_inode_count"),
782      (True, "{}_f2fs_compress", "f2fs_compress"),
783      (True, "{}_f2fs_sldc_flags", "f2fs_sldc_flags"),
784      (True, "{}_f2fs_blocksize", "f2fs_block_size"),
785      (True, "{}_reserved_size", "partition_reserved_size"),
786      (True, "{}_squashfs_block_size", "squashfs_block_size"),
787      (True, "{}_squashfs_compressor", "squashfs_compressor"),
788      (True, "{}_squashfs_compressor_opt", "squashfs_compressor_opt"),
789      (True, "{}_squashfs_disable_4k_align", "squashfs_disable_4k_align"),
790      (True, "{}_verity_block_device", "verity_block_device"),
791  )
792
793  # Translate prefixed properties into generic ones.
794  if mount_point == "data":
795    prefix = "userdata"
796  else:
797    prefix = mount_point
798
799  for readonly, src_prop, dest_prop in fmt_props:
800    if readonly and mount_point not in ro_mount_points:
801      continue
802
803    if src_prop == "fs_type":
804      # This property is legacy and only used on a few partitions. b/202600377
805      allowed_partitions = set(["system", "system_other", "data", "oem"])
806      if mount_point not in allowed_partitions:
807        continue
808
809    if (mount_point == "system_other") and (dest_prop != "partition_size"):
810      # Propagate system properties to system_other. They'll get overridden
811      # after as needed.
812      copy_prop(src_prop.format("system"), dest_prop)
813
814    copy_prop(src_prop.format(prefix), dest_prop)
815
816  # Set prefixed properties that need a default value.
817  if mount_point in ro_mount_points:
818    prop = "{}_journal_size".format(prefix)
819    if not copy_prop(prop, "journal_size"):
820      d["journal_size"] = "0"
821
822    prop = "{}_extfs_rsv_pct".format(prefix)
823    if not copy_prop(prop, "extfs_rsv_pct"):
824      d["extfs_rsv_pct"] = "0"
825
826    d["ro_mount_point"] = "1"
827
828  # Copy partition-specific properties.
829  d["mount_point"] = mount_point
830  if mount_point == "system":
831    copy_prop("system_headroom", "partition_headroom")
832    copy_prop("root_dir", "root_dir")
833    copy_prop("root_fs_config", "root_fs_config")
834  elif mount_point == "data":
835    # Copy the generic fs type first, override with specific one if available.
836    copy_prop("flash_logical_block_size", "flash_logical_block_size")
837    copy_prop("flash_erase_block_size", "flash_erase_block_size")
838    copy_prop("needs_casefold", "needs_casefold")
839    copy_prop("needs_projid", "needs_projid")
840    copy_prop("needs_compress", "needs_compress")
841  d["partition_name"] = mount_point
842  return d
843
844
845def LoadGlobalDict(filename):
846  """Load "name=value" pairs from filename"""
847  d = {}
848  f = open(filename)
849  for line in f:
850    line = line.strip()
851    if not line or line.startswith("#"):
852      continue
853    k, v = line.split("=", 1)
854    d[k] = v
855  f.close()
856  return d
857
858
859def GlobalDictFromImageProp(image_prop, mount_point):
860  d = {}
861
862  def copy_prop(src_p, dest_p):
863    if src_p in image_prop:
864      d[dest_p] = image_prop[src_p]
865      return True
866    return False
867
868  if mount_point == "system":
869    copy_prop("partition_size", "system_size")
870  elif mount_point == "system_other":
871    copy_prop("partition_size", "system_other_size")
872  elif mount_point == "vendor":
873    copy_prop("partition_size", "vendor_size")
874  elif mount_point == "odm":
875    copy_prop("partition_size", "odm_size")
876  elif mount_point == "vendor_dlkm":
877    copy_prop("partition_size", "vendor_dlkm_size")
878  elif mount_point == "odm_dlkm":
879    copy_prop("partition_size", "odm_dlkm_size")
880  elif mount_point == "system_dlkm":
881    copy_prop("partition_size", "system_dlkm_size")
882  elif mount_point == "product":
883    copy_prop("partition_size", "product_size")
884  elif mount_point == "system_ext":
885    copy_prop("partition_size", "system_ext_size")
886  return d
887
888
889def BuildVBMeta(in_dir, glob_dict, output_path):
890  """Creates a VBMeta image.
891
892  It generates the requested VBMeta image. The requested image could be for
893  top-level or chained VBMeta image, which is determined based on the name.
894
895  Args:
896    output_path: Path to generated vbmeta.img
897    partitions: A dict that's keyed by partition names with image paths as
898        values. Only valid partition names are accepted, as partitions listed
899        in common.AVB_PARTITIONS and custom partitions listed in
900        OPTIONS.info_dict.get("avb_custom_images_partition_list")
901    name: Name of the VBMeta partition, e.g. 'vbmeta', 'vbmeta_system'.
902    needed_partitions: Partitions whose descriptors should be included into the
903        generated VBMeta image.
904
905  Returns:
906    Path to the created image.
907
908  Raises:
909    AssertionError: On invalid input args.
910  """
911  vbmeta_partitions = common.AVB_PARTITIONS[:]
912  name = os.path.basename(output_path).rstrip(".img")
913  vbmeta_system = glob_dict.get("avb_vbmeta_system", "").strip()
914  vbmeta_vendor = glob_dict.get("avb_vbmeta_vendor", "").strip()
915  if "vbmeta_system" in name:
916    vbmeta_partitions = vbmeta_system.split()
917  elif "vbmeta_vendor" in name:
918    vbmeta_partitions = vbmeta_vendor.split()
919  else:
920    if vbmeta_system:
921      vbmeta_partitions = [
922          item for item in vbmeta_partitions
923          if item not in vbmeta_system.split()]
924      vbmeta_partitions.append("vbmeta_system")
925
926    if vbmeta_vendor:
927      vbmeta_partitions = [
928          item for item in vbmeta_partitions
929          if item not in vbmeta_vendor.split()]
930      vbmeta_partitions.append("vbmeta_vendor")
931
932  partitions = {part: os.path.join(in_dir, part + ".img")
933                for part in vbmeta_partitions}
934  partitions = {part: path for (part, path) in partitions.items() if os.path.exists(path)}
935  common.BuildVBMeta(output_path, partitions, name, vbmeta_partitions)
936
937
938def BuildImageOrVBMeta(input_directory, target_out, glob_dict, image_properties, out_file):
939  try:
940    if "vbmeta" in os.path.basename(out_file):
941      OPTIONS.info_dict = glob_dict
942      BuildVBMeta(input_directory, glob_dict, out_file)
943    else:
944      BuildImage(input_directory, image_properties, out_file, target_out)
945  except:
946    logger.error("Failed to build %s from %s", out_file, input_directory)
947    raise
948
949
950def CopyInputDirectory(src, dst, filter_file):
951  with open(filter_file, 'r') as f:
952    for line in f:
953      line = line.strip()
954      if not line:
955        return
956      if line != os.path.normpath(line):
957        sys.exit(f"{line}: not normalized")
958      if line.startswith("../") or line.startswith('/'):
959        sys.exit(f"{line}: escapes staging directory by starting with ../ or /")
960      full_src = os.path.join(src, line)
961      full_dst = os.path.join(dst, line)
962      if os.path.isdir(full_src):
963        os.makedirs(full_dst, exist_ok=True)
964      else:
965        os.makedirs(os.path.dirname(full_dst), exist_ok=True)
966        os.link(full_src, full_dst, follow_symlinks=False)
967
968
969def main(argv):
970  parser = argparse.ArgumentParser(
971    description="Builds output_image from the given input_directory and properties_file, and "
972    "writes the image to target_output_directory.")
973  parser.add_argument("--input-directory-filter-file",
974    help="the path to a file that contains a list of all files in the input_directory. If this "
975    "option is provided, all files under the input_directory that are not listed in this file will "
976    "be deleted before building the image. This is to work around the fact that building a module "
977    "will install in by default, so there could be files in the input_directory that are not "
978    "actually supposed to be part of the partition. The paths in this file must be relative to "
979    "input_directory.")
980  parser.add_argument("input_directory",
981    help="the staging directory to be converted to an image file")
982  parser.add_argument("properties_file",
983    help="a file containing the 'global dictionary' of properties that affect how the image is "
984    "built")
985  parser.add_argument("out_file",
986    help="the output file to write")
987  parser.add_argument("target_out",
988    help="the path to $(TARGET_OUT). Certain tools will use this to look through multiple staging "
989    "directories for fs config files.")
990  parser.add_argument("-v", action="store_true",
991                      help="Enable verbose logging", dest="verbose")
992  args = parser.parse_args()
993  if args.verbose:
994    OPTIONS.verbose = True
995
996  common.InitLogging()
997
998  glob_dict = LoadGlobalDict(args.properties_file)
999  if "mount_point" in glob_dict:
1000    # The caller knows the mount point and provides a dictionary needed by
1001    # BuildImage().
1002    image_properties = glob_dict
1003    TryParseFingerprintAndTimestamp(image_properties)
1004  else:
1005    image_filename = os.path.basename(args.out_file)
1006    mount_point = ""
1007    if image_filename == "system.img":
1008      mount_point = "system"
1009    elif image_filename == "system_other.img":
1010      mount_point = "system_other"
1011    elif image_filename == "userdata.img":
1012      mount_point = "data"
1013    elif image_filename == "cache.img":
1014      mount_point = "cache"
1015    elif image_filename == "vendor.img":
1016      mount_point = "vendor"
1017    elif image_filename == "odm.img":
1018      mount_point = "odm"
1019    elif image_filename == "vendor_dlkm.img":
1020      mount_point = "vendor_dlkm"
1021    elif image_filename == "odm_dlkm.img":
1022      mount_point = "odm_dlkm"
1023    elif image_filename == "system_dlkm.img":
1024      mount_point = "system_dlkm"
1025    elif image_filename == "oem.img":
1026      mount_point = "oem"
1027    elif image_filename == "product.img":
1028      mount_point = "product"
1029    elif image_filename == "system_ext.img":
1030      mount_point = "system_ext"
1031    elif "vbmeta" in image_filename:
1032      mount_point = "vbmeta"
1033    else:
1034      logger.error("Unknown image file name %s", image_filename)
1035      sys.exit(1)
1036
1037    if "vbmeta" != mount_point:
1038      image_properties = ImagePropFromGlobalDict(glob_dict, mount_point)
1039
1040  if args.input_directory_filter_file and not os.environ.get("BUILD_BROKEN_INCORRECT_PARTITION_IMAGES"):
1041    with tempfile.TemporaryDirectory(dir=os.path.dirname(args.input_directory)) as new_input_directory:
1042      CopyInputDirectory(args.input_directory, new_input_directory, args.input_directory_filter_file)
1043      BuildImageOrVBMeta(new_input_directory, args.target_out, glob_dict, image_properties, args.out_file)
1044  else:
1045    BuildImageOrVBMeta(args.input_directory, args.target_out, glob_dict, image_properties, args.out_file)
1046
1047
1048if __name__ == '__main__':
1049  try:
1050    main(sys.argv[1:])
1051  finally:
1052    common.Cleanup()
1053