#!/usr/bin/env python3 # # Copyright (C) 2018 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. import argparse import logging import os import pkgutil import subprocess import sys import tempfile def RunCommand(cmd, env): """Runs the given command. Args: cmd: the command represented as a list of strings. env: a dictionary of additional environment variables. Returns: A tuple of the output and the exit code. """ env_copy = os.environ.copy() env_copy.update(env) cmd[0] = FindProgram(cmd[0]) logging.info("Env: %s", env) logging.info("Running: " + " ".join(cmd)) p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env_copy, text=True) output, _ = p.communicate() return output, p.returncode def FindProgram(prog_name): """Finds the path to prog_name. Args: prog_name: the program name to find. Returns: path to the progName if found. The program is searched in the same directory where this script is located at. If not found, progName is returned. """ exec_dir = os.path.dirname(os.path.realpath(sys.argv[0])) prog_path = os.path.join(exec_dir, prog_name) if os.path.exists(prog_path): return prog_path else: return prog_name def ParseArguments(argv): """Parses the input arguments to the program.""" parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("src_dir", help="The source directory for user image.") parser.add_argument("output_file", help="The path of the output image file.") parser.add_argument("ext_variant", choices=["ext2", "ext4"], help="Variant of the extended filesystem.") parser.add_argument("mount_point", help="The mount point for user image.") parser.add_argument("fs_size", help="Size of the file system.") parser.add_argument("file_contexts", nargs='?', help="The selinux file context.") parser.add_argument("--android_sparse", "-s", action="store_true", help="Outputs an android sparse image (mke2fs).") parser.add_argument("--journal_size", "-j", help="Journal size (mke2fs).") parser.add_argument("--timestamp", "-T", help="Fake timetamp for the output image.") parser.add_argument("--fs_config", "-C", help="Path to the fs config file (e2fsdroid).") parser.add_argument("--product_out", "-D", help="Path to the directory with device specific fs" " config files (e2fsdroid).") parser.add_argument("--block_list_file", "-B", help="Path to the block list file (e2fsdroid).") parser.add_argument("--base_alloc_file_in", "-d", help="Path to the input base fs file (e2fsdroid).") parser.add_argument("--base_alloc_file_out", "-A", help="Path to the output base fs file (e2fsdroid).") parser.add_argument("--label", "-L", help="The mount point (mke2fs).") parser.add_argument("--inodes", "-i", help="The extfs inodes count (mke2fs).") parser.add_argument("--inode_size", "-I", help="The extfs inode size (mke2fs).") parser.add_argument("--reserved_percent", "-M", help="The reserved blocks percentage (mke2fs).") parser.add_argument("--flash_erase_block_size", "-e", help="The flash erase block size (mke2fs).") parser.add_argument("--flash_logical_block_size", "-o", help="The flash logical block size (mke2fs).") parser.add_argument("--mke2fs_uuid", "-U", help="The mke2fs uuid (mke2fs) .") parser.add_argument("--mke2fs_hash_seed", "-S", help="The mke2fs hash seed (mke2fs).") parser.add_argument("--share_dup_blocks", "-c", action="store_true", help="ext4 share dup blocks (e2fsdroid).") args, remainder = parser.parse_known_args(argv) # The current argparse doesn't handle intermixed arguments well. Checks # manually whether the file_contexts exists as the last argument. # TODO(xunchang) use parse_intermixed_args() when we switch to python 3.7. if len(remainder) == 1 and remainder[0] == argv[-1]: args.file_contexts = remainder[0] elif remainder: parser.print_usage() sys.exit(1) return args def ConstructE2fsCommands(args): """Builds the mke2fs & e2fsdroid command based on the input arguments. Args: args: The result of ArgumentParser after parsing the command line arguments. Returns: A tuple of two lists that serve as the command for mke2fs and e2fsdroid. """ BLOCKSIZE = 4096 e2fsdroid_opts = [] mke2fs_extended_opts = [] mke2fs_opts = [] if args.android_sparse: mke2fs_extended_opts.append("android_sparse") else: e2fsdroid_opts.append("-e") if args.timestamp: e2fsdroid_opts += ["-T", args.timestamp] if args.fs_config: e2fsdroid_opts += ["-C", args.fs_config] if args.product_out: e2fsdroid_opts += ["-p", args.product_out] if args.block_list_file: e2fsdroid_opts += ["-B", args.block_list_file] if args.base_alloc_file_in: e2fsdroid_opts += ["-d", args.base_alloc_file_in] if args.base_alloc_file_out: e2fsdroid_opts += ["-D", args.base_alloc_file_out] if args.share_dup_blocks: e2fsdroid_opts.append("-s") if args.file_contexts: e2fsdroid_opts += ["-S", args.file_contexts] if args.flash_erase_block_size: mke2fs_extended_opts.append("stripe_width={}".format( int(args.flash_erase_block_size) // BLOCKSIZE)) if args.flash_logical_block_size: # stride should be the max of 8kb and the logical block size stride = max(int(args.flash_logical_block_size), 8192) mke2fs_extended_opts.append("stride={}".format(stride // BLOCKSIZE)) if args.mke2fs_hash_seed: mke2fs_extended_opts.append("hash_seed=" + args.mke2fs_hash_seed) if args.journal_size: if args.journal_size == "0": mke2fs_opts += ["-O", "^has_journal"] else: mke2fs_opts += ["-J", "size=" + args.journal_size] if args.label: mke2fs_opts += ["-L", args.label] if args.inodes: mke2fs_opts += ["-N", args.inodes] if args.inode_size: mke2fs_opts += ["-I", args.inode_size] if args.mount_point: mke2fs_opts += ["-M", args.mount_point] if args.reserved_percent: mke2fs_opts += ["-m", args.reserved_percent] if args.mke2fs_uuid: mke2fs_opts += ["-U", args.mke2fs_uuid] if mke2fs_extended_opts: mke2fs_opts += ["-E", ','.join(mke2fs_extended_opts)] # Round down the filesystem length to be a multiple of the block size blocks = int(args.fs_size) // BLOCKSIZE mke2fs_cmd = (["mke2fs"] + mke2fs_opts + ["-t", args.ext_variant, "-b", str(BLOCKSIZE), args.output_file, str(blocks)]) e2fsdroid_cmd = (["e2fsdroid"] + e2fsdroid_opts + ["-f", args.src_dir, "-a", args.mount_point, args.output_file]) return mke2fs_cmd, e2fsdroid_cmd def main(argv): logging_format = '%(asctime)s %(filename)s %(levelname)s: %(message)s' logging.basicConfig(level=logging.INFO, format=logging_format, datefmt='%H:%M:%S') args = ParseArguments(argv) if not os.path.isdir(args.src_dir): logging.error("Can not find directory %s", args.src_dir) sys.exit(2) if not args.mount_point: logging.error("Mount point is required") sys.exit(2) if args.mount_point[0] != '/': args.mount_point = '/' + args.mount_point if not args.fs_size: logging.error("Size of the filesystem is required") sys.exit(2) mke2fs_cmd, e2fsdroid_cmd = ConstructE2fsCommands(args) # truncate output file since mke2fs will keep verity section in existing file with open(args.output_file, 'w') as output: output.truncate() # run mke2fs with tempfile.NamedTemporaryFile() as conf_file: conf_data = pkgutil.get_data('mkuserimg_mke2fs', 'mke2fs.conf') conf_file.write(conf_data) conf_file.flush() mke2fs_env = {"MKE2FS_CONFIG" : conf_file.name} if args.timestamp: mke2fs_env["E2FSPROGS_FAKE_TIME"] = args.timestamp output, ret = RunCommand(mke2fs_cmd, mke2fs_env) print(output) if ret != 0: logging.error("Failed to run mke2fs: " + output) sys.exit(4) # run e2fsdroid e2fsdroid_env = {} if args.timestamp: e2fsdroid_env["E2FSPROGS_FAKE_TIME"] = args.timestamp output, ret = RunCommand(e2fsdroid_cmd, e2fsdroid_env) # The build script is parsing the raw output of e2fsdroid; keep the pattern # unchanged for now. print(output) if ret != 0: logging.error("Failed to run e2fsdroid_cmd: " + output) os.remove(args.output_file) sys.exit(4) if __name__ == '__main__': main(sys.argv[1:])