1#!/usr/bin/env python3 2# Copyright (C) 2019 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16from __future__ import print_function 17import argparse 18import distutils 19import errno 20import grp 21import os 22import readline 23import sys 24import shutil 25import subprocess 26from pipes import quote 27from subprocess import check_call 28 29try: 30 from shutil import which as find_executable 31except AttributeError: 32 from distutils.spawn import find_executable 33 34REPO_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 35sys.path.append(os.path.join(REPO_ROOT, 'infra', 'ci')) 36from config import JOB_CONFIGS, SANDBOX_IMG 37 38try: 39 input = raw_input 40except NameError: 41 pass 42 43 44def user_in_docker_group(): 45 try: 46 group = grp.getgrnam('docker') 47 except KeyError: 48 return False 49 else: 50 return group.gr_gid in os.getgroups() 51 52 53def decision(question='Would you like to continue', confirm=True, default='n'): 54 default = default.lower().strip() 55 yes = default in {'y', 'yes'} 56 no = default in {'n', 'no'} 57 default = 'y' if yes else 'n' 58 prompt = '%s? [%s/%s]: ' % (question, 'Y' if yes else 'y', 'N' if no else 'n') 59 if not confirm: 60 print('%sy' % prompt) 61 return 62 while True: 63 choice = input(prompt).lower().strip() 64 if not choice: 65 choice = default 66 if choice in {'y', 'yes'}: 67 return 68 elif choice in {'n', 'no'}: 69 sys.exit(3) 70 71 72def main(): 73 parser = argparse.ArgumentParser( 74 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 75 parser.add_argument('config', choices=JOB_CONFIGS.keys()) 76 parser.add_argument( 77 '--runner', 78 help='The container runner executable to use', 79 choices=('podman', 'docker'), 80 default='podman' if find_executable('podman') else 'docker') 81 parser.add_argument( 82 '--build', 83 action='store_true', 84 help='Will perform a build of sandbox image') 85 group = parser.add_mutually_exclusive_group() 86 group.add_argument( 87 '--confirm', 88 action='store_true', 89 default=True, 90 help='User confirmation of decision prompts') 91 group.add_argument( 92 '--no-confirm', 93 dest='confirm', 94 action='store_false', 95 help='Forces confirmation of decision prompts') 96 args = parser.parse_args() 97 98 # Check that the directory is clean. 99 git_cmd = ['git', '-C', REPO_ROOT, 'status', '--porcelain'] 100 modified_files = subprocess.check_output(git_cmd).decode() 101 if modified_files: 102 print('The current Git repo has modified/untracked files.') 103 print('The sandboxed VM will fetch the HEAD of your current git repo.') 104 print('This is probably not the state you want to be in.') 105 print('I suggest you stop, commit and then re-run this script') 106 print('Modified files:\n' + modified_files) 107 decision('Do you know what you are doing', confirm=args.confirm) 108 109 if args.build: 110 print('') 111 print('About to build %r locally with %r' % (args.image, args.runner)) 112 decision(confirm=args.confirm) 113 check_call(('make', '-C', os.path.join(REPO_ROOT, 'infra', 'ci'), 114 'BUILDER=%s' % args.runner, 'build-sandbox')) 115 116 bundle_path = '/tmp/perfetto-ci.bundle' 117 check_call(['git', '-C', REPO_ROOT, 'bundle', 'create', bundle_path, 'HEAD']) 118 os.chmod(bundle_path, 0o664) 119 env = { 120 'PERFETTO_TEST_GIT_REF': bundle_path, 121 } 122 env.update(JOB_CONFIGS[args.config]) 123 124 workdir = os.path.join(REPO_ROOT, 'out', 'tmp.ci') 125 cmd = [] 126 if args.runner == 'docker' and not user_in_docker_group(): 127 cmd += ['sudo', '--'] 128 cmd += [ 129 args.runner, 'run', '-it', '--name', 'perfetto_ci', '--cap-add', 130 'SYS_PTRACE', '--rm', '--volume', 131 '%s:/ci/ramdisk' % workdir, '--tmpfs', '/tmp:exec', 132 '--volume=%s:%s:ro' % (bundle_path, bundle_path) 133 ] 134 for kv in env.items(): 135 cmd += ['--env', '%s=%s' % kv] 136 cmd += [SANDBOX_IMG] 137 cmd += [ 138 'bash', '-c', 139 'cd /ci/ramdisk; bash /ci/init.sh || sudo -u perfetto -EH bash -i' 140 ] 141 142 print( 143 'About to run\n', 144 ' '.join('\n ' + c if c.startswith('--') or c == 'bash' else quote(c) 145 for c in cmd)) 146 print('') 147 print('The VM workdir /ci/ramdisk will be mounted into: %s' % workdir) 148 print('The contents of %s will be deleted before starting the VM' % workdir) 149 decision(confirm=args.confirm) 150 151 try: 152 shutil.rmtree(workdir) 153 except EnvironmentError as e: 154 if e.errno == errno.ENOENT: 155 pass 156 elif e.errno == errno.EACCES: 157 print('') 158 print('Removing previous volume %r' % workdir) 159 check_call(('sudo', 'rm', '-r', quote(workdir))) 160 else: 161 raise 162 163 os.makedirs(workdir) 164 os.execvp(cmd[0], cmd) 165 166 167if __name__ == '__main__': 168 sys.exit(main()) 169