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