1# Copyright 2020 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Runs a command inside an NsJail sandbox for building Android.
16
17NsJail creates a user namespace sandbox where
18Android can be built in an isolated process.
19If no command is provided then it will open
20an interactive bash shell.
21"""
22
23import argparse
24import collections
25import os
26import re
27import subprocess
28from . import config
29from .overlay import BindMount
30from .overlay import BindOverlay
31
32_DEFAULT_META_ANDROID_DIR = 'LINUX/android'
33_DEFAULT_COMMAND = '/bin/bash'
34
35_SOURCE_MOUNT_POINT = '/src'
36_OUT_MOUNT_POINT = '/src/out'
37_DIST_MOUNT_POINT = '/dist'
38_META_MOUNT_POINT = '/meta'
39
40_CHROOT_MOUNT_POINTS = [
41  'bin', 'sbin',
42  'etc/alternatives', 'etc/default', 'etc/perl',
43  'etc/ssl', 'etc/xml',
44  'lib', 'lib32', 'lib64', 'libx32',
45  'usr',
46]
47
48
49def run(command,
50        build_target,
51        nsjail_bin,
52        chroot,
53        overlay_config=None,
54        source_dir=os.getcwd(),
55        dist_dir=None,
56        build_id=None,
57        out_dir = None,
58        meta_root_dir = None,
59        meta_android_dir = _DEFAULT_META_ANDROID_DIR,
60        mount_local_device = False,
61        max_cpus=None,
62        extra_bind_mounts=[],
63        readonly_bind_mounts=[],
64        extra_nsjail_args=[],
65        dry_run=False,
66        quiet=False,
67        env=[],
68        nsjail_wrapper=[],
69        stdout=None,
70        stderr=None):
71  """Run inside an NsJail sandbox.
72
73  Args:
74    command: A list of strings with the command to run.
75    build_target: A string with the name of the build target to be prepared
76      inside the container.
77    nsjail_bin: A string with the path to the nsjail binary.
78    chroot: A string with the path to the chroot.
79    overlay_config: A string path to an overlay configuration file.
80    source_dir: A string with the path to the Android platform source.
81    dist_dir: A string with the path to the dist directory.
82    build_id: A string with the build identifier.
83    out_dir: An optional path to the Android build out folder.
84    meta_root_dir: An optional path to a folder containing the META build.
85    meta_android_dir: An optional path to the location where the META build expects
86      the Android build. This path must be relative to meta_root_dir.
87    mount_local_device: Whether to mount /dev/usb (and related) trees enabling
88      adb to run inside the jail
89    max_cpus: An integer with maximum number of CPUs.
90    extra_bind_mounts: An array of extra mounts in the 'source' or 'source:dest' syntax.
91    readonly_bind_mounts: An array of read only mounts in the 'source' or 'source:dest' syntax.
92    extra_nsjail_args: A list of strings that contain extra arguments to nsjail.
93    dry_run: If true, the command will be returned but not executed
94    quiet: If true, the function will not display the command and
95      will pass -quiet argument to nsjail
96    env: An array of environment variables to define in the jail in the `var=val` syntax.
97    nsjail_wrapper: A list of strings used to wrap the nsjail command.
98    stdout: the standard output for all printed messages. Valid values are None, a file
99      descriptor or file object. A None value means sys.stdout is used.
100    stderr: the standard error for all printed messages. Valid values are None, a file
101      descriptor or file object, and subprocess.STDOUT (which indicates that all stderr
102      should be redirected to stdout). A None value means sys.stderr is used.
103
104  Returns:
105    A list of strings with the command executed.
106  """
107
108
109  nsjail_command = get_command(
110      command=command,
111      build_target=build_target,
112      nsjail_bin=nsjail_bin,
113      chroot=chroot,
114      cfg=config.factory(overlay_config),
115      source_dir=source_dir,
116      dist_dir=dist_dir,
117      build_id=build_id,
118      out_dir=out_dir,
119      meta_root_dir=meta_root_dir,
120      meta_android_dir=meta_android_dir,
121      mount_local_device=mount_local_device,
122      max_cpus=max_cpus,
123      extra_bind_mounts=extra_bind_mounts,
124      readonly_bind_mounts=readonly_bind_mounts,
125      extra_nsjail_args=extra_nsjail_args,
126      quiet=quiet,
127      env=env,
128      nsjail_wrapper=nsjail_wrapper)
129
130  run_command(
131      nsjail_command=nsjail_command,
132      mount_local_device=mount_local_device,
133      dry_run=dry_run,
134      quiet=quiet,
135      stdout=stdout,
136      stderr=stderr)
137
138  return nsjail_command
139
140def get_command(command,
141        build_target,
142        nsjail_bin,
143        chroot,
144        cfg=None,
145        source_dir=os.getcwd(),
146        dist_dir=None,
147        build_id=None,
148        out_dir = None,
149        meta_root_dir = None,
150        meta_android_dir = _DEFAULT_META_ANDROID_DIR,
151        mount_local_device = False,
152        max_cpus=None,
153        extra_bind_mounts=[],
154        readonly_bind_mounts=[],
155        extra_nsjail_args=[],
156        quiet=False,
157        env=[],
158        nsjail_wrapper=[]):
159  """Get command to run nsjail sandbox.
160
161  Args:
162    command: A list of strings with the command to run.
163    build_target: A string with the name of the build target to be prepared
164      inside the container.
165    nsjail_bin: A string with the path to the nsjail binary.
166    chroot: A string with the path to the chroot.
167    cfg: A config.Config instance or None.
168    source_dir: A string with the path to the Android platform source.
169    dist_dir: A string with the path to the dist directory.
170    build_id: A string with the build identifier.
171    out_dir: An optional path to the Android build out folder.
172    meta_root_dir: An optional path to a folder containing the META build.
173    meta_android_dir: An optional path to the location where the META build expects
174      the Android build. This path must be relative to meta_root_dir.
175    max_cpus: An integer with maximum number of CPUs.
176    extra_bind_mounts: An array of extra mounts in the 'source' or 'source:dest' syntax.
177    readonly_bind_mounts: An array of read only mounts in the 'source' or 'source:dest' syntax.
178    extra_nsjail_args: A list of strings that contain extra arguments to nsjail.
179    quiet: If true, the function will not display the command and
180      will pass -quiet argument to nsjail
181    env: An array of environment variables to define in the jail in the `var=val` syntax.
182
183  Returns:
184    A list of strings with the command to execute.
185  """
186  script_dir = os.path.dirname(os.path.abspath(__file__))
187  config_file = os.path.join(script_dir, 'nsjail.cfg')
188
189  # Run expects absolute paths
190  if out_dir:
191    out_dir = os.path.abspath(out_dir)
192  if dist_dir:
193    dist_dir = os.path.abspath(dist_dir)
194  if meta_root_dir:
195    meta_root_dir = os.path.abspath(meta_root_dir)
196  if source_dir:
197    source_dir = os.path.abspath(source_dir)
198
199  if nsjail_bin:
200    nsjail_bin = os.path.join(source_dir, nsjail_bin)
201
202  if chroot:
203    chroot = os.path.join(source_dir, chroot)
204
205  if meta_root_dir:
206    if not meta_android_dir or os.path.isabs(meta_android_dir):
207      raise ValueError('error: the provided meta_android_dir is not a path'
208          'relative to meta_root_dir.')
209
210  nsjail_command = nsjail_wrapper + [nsjail_bin,
211    '--env', 'USER=nobody',
212    '--config', config_file]
213
214  # By mounting the points individually that we need we reduce exposure and
215  # keep the chroot clean from artifacts
216  if chroot:
217    for mpoints in _CHROOT_MOUNT_POINTS:
218      source = os.path.join(chroot, mpoints)
219      dest = os.path.join('/', mpoints)
220      if os.path.exists(source):
221        nsjail_command.extend([
222          '--bindmount_ro', '%s:%s' % (source, dest)
223        ])
224
225  if build_id:
226    nsjail_command.extend(['--env', 'BUILD_NUMBER=%s' % build_id])
227  if max_cpus:
228    nsjail_command.append('--max_cpus=%i' % max_cpus)
229  if quiet:
230    nsjail_command.append('--quiet')
231
232  whiteout_list = set()
233  if out_dir and (
234      os.path.dirname(out_dir) == source_dir) and (
235      os.path.basename(out_dir) != 'out'):
236    whiteout_list.add(os.path.abspath(out_dir))
237    if not os.path.exists(out_dir):
238      os.makedirs(out_dir)
239
240  # Apply the overlay for the selected Android target to the source directory
241  # from the supplied config.Config instance (which may be None).
242  if cfg is not None:
243    overlay = BindOverlay(build_target,
244                      source_dir,
245                      cfg,
246                      whiteout_list,
247                      _SOURCE_MOUNT_POINT,
248                      quiet=quiet)
249    bind_mounts = overlay.GetBindMounts()
250  else:
251    bind_mounts = collections.OrderedDict()
252    bind_mounts[_SOURCE_MOUNT_POINT] = BindMount(source_dir, False)
253
254  if out_dir:
255    bind_mounts[_OUT_MOUNT_POINT] = BindMount(out_dir, False)
256
257  if dist_dir:
258    bind_mounts[_DIST_MOUNT_POINT] = BindMount(dist_dir, False)
259    nsjail_command.extend([
260        '--env', 'DIST_DIR=%s'%_DIST_MOUNT_POINT
261    ])
262
263  if meta_root_dir:
264    bind_mounts[_META_MOUNT_POINT] = BindMount(meta_root_dir, False)
265    bind_mounts[os.path.join(_META_MOUNT_POINT, meta_android_dir)] = BindMount(source_dir, False)
266    if out_dir:
267      bind_mounts[os.path.join(_META_MOUNT_POINT, meta_android_dir, 'out')] = BindMount(out_dir, False)
268
269  for bind_destination, bind_mount in bind_mounts.items():
270    if bind_mount.readonly:
271      nsjail_command.extend([
272        '--bindmount_ro',  bind_mount.source_dir + ':' + bind_destination
273      ])
274    else:
275      nsjail_command.extend([
276        '--bindmount',  bind_mount.source_dir + ':' + bind_destination
277      ])
278
279  if mount_local_device:
280    # Mount /dev/bus/usb and several /sys/... paths, which adb will examine
281    # while attempting to find the attached android device. These paths expose
282    # a lot of host operating system device space, so it's recommended to use
283    # the mount_local_device option only when you need to use adb (e.g., for
284    # atest or some other purpose).
285    nsjail_command.extend(['--bindmount', '/dev/bus/usb'])
286    nsjail_command.extend(['--bindmount', '/sys/bus/usb/devices'])
287    nsjail_command.extend(['--bindmount', '/sys/dev'])
288    nsjail_command.extend(['--bindmount', '/sys/devices'])
289
290  for mount in extra_bind_mounts:
291    nsjail_command.extend(['--bindmount', mount])
292  for mount in readonly_bind_mounts:
293    nsjail_command.extend(['--bindmount_ro', mount])
294
295  for var in env:
296    nsjail_command.extend(['--env', var])
297
298  nsjail_command.extend(extra_nsjail_args)
299
300  nsjail_command.append('--')
301  nsjail_command.extend(command)
302
303  return nsjail_command
304
305def run_command(nsjail_command,
306                mount_local_device=False,
307                dry_run=False,
308                quiet=False,
309                stdout=None,
310                stderr=None):
311  """Run the provided nsjail command.
312
313  Args:
314    nsjail_command: A list of strings with the command to run.
315    mount_local_device: Whether to mount /dev/usb (and related) trees enabling
316      adb to run inside the jail
317    dry_run: If true, the command will be returned but not executed
318    quiet: If true, the function will not display the command and
319      will pass -quiet argument to nsjail
320    stdout: the standard output for all printed messages. Valid values are None, a file
321      descriptor or file object. A None value means sys.stdout is used.
322    stderr: the standard error for all printed messages. Valid values are None, a file
323      descriptor or file object, and subprocess.STDOUT (which indicates that all stderr
324      should be redirected to stdout). A None value means sys.stderr is used.
325  """
326
327  if mount_local_device:
328    # A device can only communicate with one adb server at a time, so the adb server is
329    # killed on the host machine.
330    for line in subprocess.check_output(['ps','-eo','cmd']).decode().split('\n'):
331      if re.match(r'adb.*fork-server.*', line):
332        print('An adb server is running on your host machine. This server must be '
333              'killed to use the --mount_local_device flag.')
334        print('Continue? [y/N]: ', end='')
335        if input().lower() != 'y':
336          exit()
337        subprocess.check_call(['adb', 'kill-server'])
338
339  if not quiet:
340    print('NsJail command:', file=stdout)
341    print(' '.join(nsjail_command), file=stdout)
342
343  if not dry_run:
344    subprocess.check_call(nsjail_command, stdout=stdout, stderr=stderr)
345
346def parse_args():
347  """Parse command line arguments.
348
349  Returns:
350    An argparse.Namespace object.
351  """
352
353  # Use the top level module docstring for the help description
354  parser = argparse.ArgumentParser(
355      description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
356  parser.add_argument(
357      '--nsjail_bin',
358      required=True,
359      help='Path to NsJail binary.')
360  parser.add_argument(
361      '--chroot',
362      help='Path to the chroot to be used for building the Android'
363      'platform. This will be mounted as the root filesystem in the'
364      'NsJail sandbox.')
365  parser.add_argument(
366      '--overlay_config',
367      help='Path to the overlay configuration file.')
368  parser.add_argument(
369      '--source_dir',
370      default=os.getcwd(),
371      help='Path to Android platform source to be mounted as /src.')
372  parser.add_argument(
373      '--out_dir',
374      help='Full path to the Android build out folder. If not provided, uses '
375      'the standard \'out\' folder in the current path.')
376  parser.add_argument(
377      '--meta_root_dir',
378      default='',
379      help='Full path to META folder. Default to \'\'')
380  parser.add_argument(
381      '--meta_android_dir',
382      default=_DEFAULT_META_ANDROID_DIR,
383      help='Relative path to the location where the META build expects '
384      'the Android build. This path must be relative to meta_root_dir. '
385      'Defaults to \'%s\'' % _DEFAULT_META_ANDROID_DIR)
386  parser.add_argument(
387      '--command',
388      default=_DEFAULT_COMMAND,
389      help='Command to run after entering the NsJail.'
390      'If not set then an interactive Bash shell will be launched')
391  parser.add_argument(
392      '--build_target',
393      required=True,
394      help='Android target selected for building')
395  parser.add_argument(
396      '--dist_dir',
397      help='Path to the Android dist directory. This is where'
398      'Android platform release artifacts will be written.'
399      'If unset then the Android platform default will be used.')
400  parser.add_argument(
401      '--build_id',
402      help='Build identifier what will label the Android platform'
403      'release artifacts.')
404  parser.add_argument(
405      '--max_cpus',
406      type=int,
407      help='Limit of concurrent CPU cores that the NsJail sandbox'
408      'can use. Defaults to unlimited.')
409  parser.add_argument(
410      '--bindmount',
411      type=str,
412      default=[],
413      action='append',
414      help='List of mountpoints to be mounted. Can be specified multiple times. '
415      'Syntax: \'source\' or \'source:dest\'')
416  parser.add_argument(
417      '--bindmount_ro',
418      type=str,
419      default=[],
420      action='append',
421      help='List of mountpoints to be mounted read-only. Can be specified multiple times. '
422      'Syntax: \'source\' or \'source:dest\'')
423  parser.add_argument(
424      '--dry_run',
425      action='store_true',
426      help='Prints the command without executing')
427  parser.add_argument(
428      '--quiet', '-q',
429      action='store_true',
430      help='Suppress debugging output')
431  parser.add_argument(
432      '--mount_local_device',
433      action='store_true',
434      help='If provided, mount locally connected Android USB devices inside '
435      'the container. WARNING: Using this flag will cause the adb server to be '
436      'killed on the host machine. WARNING: Using this flag exposes parts of '
437      'the host /sys/... file system. Use only when you need adb.')
438  parser.add_argument(
439      '--env', '-e',
440      type=str,
441      default=[],
442      action='append',
443      help='Specify an environment variable to the NSJail sandbox. Can be specified '
444      'muliple times. Syntax: var_name=value')
445  return parser.parse_args()
446
447def run_with_args(args):
448  """Run inside an NsJail sandbox.
449
450  Use the arguments from an argspace namespace.
451
452  Args:
453    An argparse.Namespace object.
454
455  Returns:
456    A list of strings with the commands executed.
457  """
458  run(chroot=args.chroot,
459      nsjail_bin=args.nsjail_bin,
460      overlay_config=args.overlay_config,
461      source_dir=args.source_dir,
462      command=args.command.split(),
463      build_target=args.build_target,
464      dist_dir=args.dist_dir,
465      build_id=args.build_id,
466      out_dir=args.out_dir,
467      meta_root_dir=args.meta_root_dir,
468      meta_android_dir=args.meta_android_dir,
469      mount_local_device=args.mount_local_device,
470      max_cpus=args.max_cpus,
471      extra_bind_mounts=args.bindmount,
472      readonly_bind_mounts=args.bindmount_ro,
473      dry_run=args.dry_run,
474      quiet=args.quiet,
475      env=args.env)
476
477def main():
478  run_with_args(parse_args())
479
480if __name__ == '__main__':
481  main()
482