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', 114 'ci'), 115 'BUILDER=%s' % args.runner, 'build-sandbox')) 116 117 bundle_path = '/tmp/perfetto-ci.bundle' 118 check_call(['git', '-C', REPO_ROOT, 'bundle', 'create', bundle_path, 'HEAD' ]) 119 os.chmod(bundle_path, 0o664) 120 env = { 121 'PERFETTO_TEST_GIT_REF': bundle_path, 122 } 123 env.update(JOB_CONFIGS[args.config]) 124 125 workdir = os.path.join(REPO_ROOT, 'out', 'tmp.ci') 126 cmd = [] 127 if args.runner == 'docker' and not user_in_docker_group(): 128 cmd += ['sudo', '--'] 129 cmd += [ 130 args.runner, 'run', '-it', '--name', 'perfetto_ci', '--cap-add', 131 'SYS_PTRACE', '--rm', '--volume', 132 '%s:/ci/ramdisk' % workdir, '--tmpfs', '/tmp:exec', 133 '--volume=%s:%s:ro' % (bundle_path, bundle_path) 134 ] 135 for kv in env.items(): 136 cmd += ['--env', '%s=%s' % kv] 137 cmd += [SANDBOX_IMG] 138 cmd += [ 139 'bash', '-c', 140 'cd /ci/ramdisk; bash /ci/init.sh || sudo -u perfetto -EH bash -i' 141 ] 142 143 print( 144 'About to run\n', 145 ' '.join('\n ' + c if c.startswith('--') or c == 'bash' else quote(c) 146 for c in cmd)) 147 print('') 148 print('The VM workdir /ci/ramdisk will be mounted into: %s' % workdir) 149 print('The contents of %s will be deleted before starting the VM' % workdir) 150 decision(confirm=args.confirm) 151 152 try: 153 shutil.rmtree(workdir) 154 except EnvironmentError as e: 155 if e.errno == errno.ENOENT: 156 pass 157 elif e.errno == errno.EACCES: 158 print('') 159 print('Removing previous volume %r' % workdir) 160 check_call(('sudo', 'rm', '-r', quote(workdir))) 161 else: 162 raise 163 164 os.makedirs(workdir) 165 os.execvp(cmd[0], cmd) 166 167 168if __name__ == '__main__': 169 sys.exit(main()) 170