1#!/usr/bin/env python
2#
3# Copyright (C) 2018 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
17import argparse
18import logging
19import os
20import pkgutil
21import subprocess
22import sys
23import tempfile
24
25
26def RunCommand(cmd, env):
27  """Runs the given command.
28
29  Args:
30    cmd: the command represented as a list of strings.
31    env: a dictionary of additional environment variables.
32  Returns:
33    A tuple of the output and the exit code.
34  """
35  env_copy = os.environ.copy()
36  env_copy.update(env)
37
38  cmd[0] = FindProgram(cmd[0])
39
40  logging.info("Env: %s", env)
41  logging.info("Running: " + " ".join(cmd))
42
43  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
44                       env=env_copy)
45  output, _ = p.communicate()
46
47  return output, p.returncode
48
49def FindProgram(prog_name):
50  """Finds the path to prog_name.
51
52  Args:
53    prog_name: the program name to find.
54  Returns:
55    path to the progName if found. The program is searched in the same directory
56    where this script is located at. If not found, progName is returned.
57  """
58  exec_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
59  prog_path = os.path.join(exec_dir, prog_name)
60  if os.path.exists(prog_path):
61    return prog_path
62  else:
63    return prog_name
64
65def ParseArguments(argv):
66  """Parses the input arguments to the program."""
67
68  parser = argparse.ArgumentParser(
69      description=__doc__,
70      formatter_class=argparse.RawDescriptionHelpFormatter)
71
72  parser.add_argument("src_dir", help="The source directory for user image.")
73  parser.add_argument("output_file", help="The path of the output image file.")
74  parser.add_argument("ext_variant", choices=["ext2", "ext4"],
75                      help="Variant of the extended filesystem.")
76  parser.add_argument("mount_point", help="The mount point for user image.")
77  parser.add_argument("fs_size", help="Size of the file system.")
78  parser.add_argument("file_contexts", nargs='?',
79                      help="The selinux file context.")
80
81  parser.add_argument("--android_sparse", "-s", action="store_true",
82                      help="Outputs an android sparse image (mke2fs).")
83  parser.add_argument("--journal_size", "-j",
84                      help="Journal size (mke2fs).")
85  parser.add_argument("--timestamp", "-T",
86                      help="Fake timetamp for the output image.")
87  parser.add_argument("--fs_config", "-C",
88                      help="Path to the fs config file (e2fsdroid).")
89  parser.add_argument("--product_out", "-D",
90                      help="Path to the directory with device specific fs"
91                           " config files (e2fsdroid).")
92  parser.add_argument("--block_list_file", "-B",
93                      help="Path to the block list file (e2fsdroid).")
94  parser.add_argument("--base_alloc_file_in", "-d",
95                      help="Path to the input base fs file (e2fsdroid).")
96  parser.add_argument("--base_alloc_file_out", "-A",
97                      help="Path to the output base fs file (e2fsdroid).")
98  parser.add_argument("--label", "-L",
99                      help="The mount point (mke2fs).")
100  parser.add_argument("--inodes", "-i",
101                      help="The extfs inodes count (mke2fs).")
102  parser.add_argument("--inode_size", "-I",
103                      help="The extfs inode size (mke2fs).")
104  parser.add_argument("--reserved_percent", "-M",
105                      help="The reserved blocks percentage (mke2fs).")
106  parser.add_argument("--flash_erase_block_size", "-e",
107                      help="The flash erase block size (mke2fs).")
108  parser.add_argument("--flash_logical_block_size", "-o",
109                      help="The flash logical block size (mke2fs).")
110  parser.add_argument("--mke2fs_uuid", "-U",
111                      help="The mke2fs uuid (mke2fs) .")
112  parser.add_argument("--mke2fs_hash_seed", "-S",
113                      help="The mke2fs hash seed (mke2fs).")
114  parser.add_argument("--share_dup_blocks", "-c", action="store_true",
115                      help="ext4 share dup blocks (e2fsdroid).")
116
117  args, remainder = parser.parse_known_args(argv)
118  # The current argparse doesn't handle intermixed arguments well. Checks
119  # manually whether the file_contexts exists as the last argument.
120  # TODO(xunchang) use parse_intermixed_args() when we switch to python 3.7.
121  if len(remainder) == 1 and remainder[0] == argv[-1]:
122    args.file_contexts = remainder[0]
123  elif remainder:
124    parser.print_usage()
125    sys.exit(1)
126
127  return args
128
129
130def ConstructE2fsCommands(args):
131  """Builds the mke2fs & e2fsdroid command based on the input arguments.
132
133  Args:
134    args: The result of ArgumentParser after parsing the command line arguments.
135  Returns:
136    A tuple of two lists that serve as the command for mke2fs and e2fsdroid.
137  """
138
139  BLOCKSIZE = 4096
140
141  e2fsdroid_opts = []
142  mke2fs_extended_opts = []
143  mke2fs_opts = []
144
145  if args.android_sparse:
146    mke2fs_extended_opts.append("android_sparse")
147  else:
148    e2fsdroid_opts.append("-e")
149  if args.timestamp:
150    e2fsdroid_opts += ["-T", args.timestamp]
151  if args.fs_config:
152    e2fsdroid_opts += ["-C", args.fs_config]
153  if args.product_out:
154    e2fsdroid_opts += ["-p", args.product_out]
155  if args.block_list_file:
156    e2fsdroid_opts += ["-B", args.block_list_file]
157  if args.base_alloc_file_in:
158    e2fsdroid_opts += ["-d", args.base_alloc_file_in]
159  if args.base_alloc_file_out:
160    e2fsdroid_opts += ["-D", args.base_alloc_file_out]
161  if args.share_dup_blocks:
162    e2fsdroid_opts.append("-s")
163  if args.file_contexts:
164    e2fsdroid_opts += ["-S", args.file_contexts]
165
166  if args.flash_erase_block_size:
167    mke2fs_extended_opts.append("stripe_width={}".format(
168        int(args.flash_erase_block_size) / BLOCKSIZE))
169  if args.flash_logical_block_size:
170    # stride should be the max of 8kb and the logical block size
171    stride = max(int(args.flash_logical_block_size), 8192)
172    mke2fs_extended_opts.append("stride={}".format(stride / BLOCKSIZE))
173  if args.mke2fs_hash_seed:
174    mke2fs_extended_opts.append("hash_seed=" + args.mke2fs_hash_seed)
175
176  if args.journal_size:
177    if args.journal_size == "0":
178      mke2fs_opts += ["-O", "^has_journal"]
179    else:
180      mke2fs_opts += ["-J", "size=" + args.journal_size]
181  if args.label:
182    mke2fs_opts += ["-L", args.label]
183  if args.inodes:
184    mke2fs_opts += ["-N", args.inodes]
185  if args.inode_size:
186    mke2fs_opts += ["-I", args.inode_size]
187  if args.mount_point:
188    mke2fs_opts += ["-M", args.mount_point]
189  if args.reserved_percent:
190    mke2fs_opts += ["-m", args.reserved_percent]
191  if args.mke2fs_uuid:
192    mke2fs_opts += ["-U", args.mke2fs_uuid]
193  if mke2fs_extended_opts:
194    mke2fs_opts += ["-E", ','.join(mke2fs_extended_opts)]
195
196  # Round down the filesystem length to be a multiple of the block size
197  blocks = int(args.fs_size) / BLOCKSIZE
198  mke2fs_cmd = (["mke2fs"] + mke2fs_opts +
199                ["-t", args.ext_variant, "-b", str(BLOCKSIZE), args.output_file,
200                 str(blocks)])
201
202  e2fsdroid_cmd = (["e2fsdroid"] + e2fsdroid_opts +
203                   ["-f", args.src_dir, "-a", args.mount_point,
204                    args.output_file])
205
206  return mke2fs_cmd, e2fsdroid_cmd
207
208
209def main(argv):
210  logging_format = '%(asctime)s %(filename)s %(levelname)s: %(message)s'
211  logging.basicConfig(level=logging.INFO, format=logging_format,
212                      datefmt='%H:%M:%S')
213
214  args = ParseArguments(argv)
215  if not os.path.isdir(args.src_dir):
216    logging.error("Can not find directory %s", args.src_dir)
217    sys.exit(2)
218  if not args.mount_point:
219    logging.error("Mount point is required")
220    sys.exit(2)
221  if args.mount_point[0] != '/':
222    args.mount_point = '/' + args.mount_point
223  if not args.fs_size:
224    logging.error("Size of the filesystem is required")
225    sys.exit(2)
226
227  mke2fs_cmd, e2fsdroid_cmd = ConstructE2fsCommands(args)
228
229  # truncate output file since mke2fs will keep verity section in existing file
230  with open(args.output_file, 'w') as output:
231    output.truncate()
232
233  # run mke2fs
234  with tempfile.NamedTemporaryFile() as conf_file:
235    conf_data = pkgutil.get_data('mkuserimg_mke2fs', 'mke2fs.conf')
236    conf_file.write(conf_data)
237    conf_file.flush()
238    mke2fs_env = {"MKE2FS_CONFIG" : conf_file.name}
239
240    if args.timestamp:
241      mke2fs_env["E2FSPROGS_FAKE_TIME"] = args.timestamp
242
243    output, ret = RunCommand(mke2fs_cmd, mke2fs_env)
244    print(output)
245    if ret != 0:
246      logging.error("Failed to run mke2fs: " + output)
247      sys.exit(4)
248
249  # run e2fsdroid
250  e2fsdroid_env = {}
251  if args.timestamp:
252    e2fsdroid_env["E2FSPROGS_FAKE_TIME"] = args.timestamp
253
254  output, ret = RunCommand(e2fsdroid_cmd, e2fsdroid_env)
255  # The build script is parsing the raw output of e2fsdroid; keep the pattern
256  # unchanged for now.
257  print(output)
258  if ret != 0:
259    logging.error("Failed to run e2fsdroid_cmd: " + output)
260    os.remove(args.output_file)
261    sys.exit(4)
262
263
264if __name__ == '__main__':
265  main(sys.argv[1:])
266