1*6777b538SAndroid Build Coastguard Worker#!/usr/bin/env python3 2*6777b538SAndroid Build Coastguard Worker# Copyright 2019 The Chromium Authors 3*6777b538SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be 4*6777b538SAndroid Build Coastguard Worker# found in the LICENSE file. 5*6777b538SAndroid Build Coastguard Worker 6*6777b538SAndroid Build Coastguard Worker"""Wraps an executable and any provided arguments into an executable script.""" 7*6777b538SAndroid Build Coastguard Worker 8*6777b538SAndroid Build Coastguard Workerimport argparse 9*6777b538SAndroid Build Coastguard Workerimport os 10*6777b538SAndroid Build Coastguard Workerimport sys 11*6777b538SAndroid Build Coastguard Workerimport textwrap 12*6777b538SAndroid Build Coastguard Worker 13*6777b538SAndroid Build Coastguard Worker 14*6777b538SAndroid Build Coastguard Worker# The bash template passes the python script into vpython via stdin. 15*6777b538SAndroid Build Coastguard Worker# The interpreter doesn't know about the script, so we have bash 16*6777b538SAndroid Build Coastguard Worker# inject the script location. 17*6777b538SAndroid Build Coastguard WorkerBASH_TEMPLATE = textwrap.dedent("""\ 18*6777b538SAndroid Build Coastguard Worker #!/usr/bin/env vpython3 19*6777b538SAndroid Build Coastguard Worker _SCRIPT_LOCATION = __file__ 20*6777b538SAndroid Build Coastguard Worker {script} 21*6777b538SAndroid Build Coastguard Worker """) 22*6777b538SAndroid Build Coastguard Worker 23*6777b538SAndroid Build Coastguard Worker 24*6777b538SAndroid Build Coastguard Worker# The batch template reruns the batch script with vpython, with the -x 25*6777b538SAndroid Build Coastguard Worker# flag instructing the interpreter to ignore the first line. The interpreter 26*6777b538SAndroid Build Coastguard Worker# knows about the (batch) script in this case, so it can get the file location 27*6777b538SAndroid Build Coastguard Worker# directly. 28*6777b538SAndroid Build Coastguard WorkerBATCH_TEMPLATE = textwrap.dedent("""\ 29*6777b538SAndroid Build Coastguard Worker @SETLOCAL ENABLEDELAYEDEXPANSION \ 30*6777b538SAndroid Build Coastguard Worker & vpython3.bat -x "%~f0" %* \ 31*6777b538SAndroid Build Coastguard Worker & EXIT /B !ERRORLEVEL! 32*6777b538SAndroid Build Coastguard Worker _SCRIPT_LOCATION = __file__ 33*6777b538SAndroid Build Coastguard Worker {script} 34*6777b538SAndroid Build Coastguard Worker """) 35*6777b538SAndroid Build Coastguard Worker 36*6777b538SAndroid Build Coastguard Worker 37*6777b538SAndroid Build Coastguard WorkerSCRIPT_TEMPLATES = { 38*6777b538SAndroid Build Coastguard Worker 'bash': BASH_TEMPLATE, 39*6777b538SAndroid Build Coastguard Worker 'batch': BATCH_TEMPLATE, 40*6777b538SAndroid Build Coastguard Worker} 41*6777b538SAndroid Build Coastguard Worker 42*6777b538SAndroid Build Coastguard Worker 43*6777b538SAndroid Build Coastguard WorkerPY_TEMPLATE = textwrap.dedent(r""" 44*6777b538SAndroid Build Coastguard Worker import os 45*6777b538SAndroid Build Coastguard Worker import re 46*6777b538SAndroid Build Coastguard Worker import shlex 47*6777b538SAndroid Build Coastguard Worker import signal 48*6777b538SAndroid Build Coastguard Worker import subprocess 49*6777b538SAndroid Build Coastguard Worker import sys 50*6777b538SAndroid Build Coastguard Worker import time 51*6777b538SAndroid Build Coastguard Worker 52*6777b538SAndroid Build Coastguard Worker _WRAPPED_PATH_RE = re.compile(r'@WrappedPath\(([^)]+)\)') 53*6777b538SAndroid Build Coastguard Worker _PATH_TO_OUTPUT_DIR = '{path_to_output_dir}' 54*6777b538SAndroid Build Coastguard Worker _SCRIPT_DIR = os.path.dirname(os.path.realpath(_SCRIPT_LOCATION)) 55*6777b538SAndroid Build Coastguard Worker 56*6777b538SAndroid Build Coastguard Worker 57*6777b538SAndroid Build Coastguard Worker def ExpandWrappedPath(arg): 58*6777b538SAndroid Build Coastguard Worker m = _WRAPPED_PATH_RE.match(arg) 59*6777b538SAndroid Build Coastguard Worker if m: 60*6777b538SAndroid Build Coastguard Worker relpath = os.path.join( 61*6777b538SAndroid Build Coastguard Worker os.path.relpath(_SCRIPT_DIR), _PATH_TO_OUTPUT_DIR, m.group(1)) 62*6777b538SAndroid Build Coastguard Worker npath = os.path.normpath(relpath) 63*6777b538SAndroid Build Coastguard Worker if os.path.sep not in npath: 64*6777b538SAndroid Build Coastguard Worker # If the original path points to something in the current directory, 65*6777b538SAndroid Build Coastguard Worker # returning the normalized version of it can be a problem. 66*6777b538SAndroid Build Coastguard Worker # normpath() strips off the './' part of the path 67*6777b538SAndroid Build Coastguard Worker # ('./foo' becomes 'foo'), which can be a problem if the result 68*6777b538SAndroid Build Coastguard Worker # is passed to something like os.execvp(); in that case 69*6777b538SAndroid Build Coastguard Worker # osexecvp() will search $PATH for the executable, rather than 70*6777b538SAndroid Build Coastguard Worker # just execing the arg directly, and if '.' isn't in $PATH, this 71*6777b538SAndroid Build Coastguard Worker # results in an error. 72*6777b538SAndroid Build Coastguard Worker # 73*6777b538SAndroid Build Coastguard Worker # So, we need to explicitly return './foo' (or '.\\foo' on windows) 74*6777b538SAndroid Build Coastguard Worker # instead of 'foo'. 75*6777b538SAndroid Build Coastguard Worker # 76*6777b538SAndroid Build Coastguard Worker # Hopefully there are no cases where this causes a problem; if 77*6777b538SAndroid Build Coastguard Worker # there are, we will either need to change the interface to 78*6777b538SAndroid Build Coastguard Worker # WrappedPath() somehow to distinguish between the two, or 79*6777b538SAndroid Build Coastguard Worker # somehow ensure that the wrapped executable doesn't hit cases 80*6777b538SAndroid Build Coastguard Worker # like this. 81*6777b538SAndroid Build Coastguard Worker return '.' + os.path.sep + npath 82*6777b538SAndroid Build Coastguard Worker return npath 83*6777b538SAndroid Build Coastguard Worker return arg 84*6777b538SAndroid Build Coastguard Worker 85*6777b538SAndroid Build Coastguard Worker 86*6777b538SAndroid Build Coastguard Worker def ExpandWrappedPaths(args): 87*6777b538SAndroid Build Coastguard Worker for i, arg in enumerate(args): 88*6777b538SAndroid Build Coastguard Worker args[i] = ExpandWrappedPath(arg) 89*6777b538SAndroid Build Coastguard Worker return args 90*6777b538SAndroid Build Coastguard Worker 91*6777b538SAndroid Build Coastguard Worker 92*6777b538SAndroid Build Coastguard Worker def FindIsolatedOutdir(raw_args): 93*6777b538SAndroid Build Coastguard Worker outdir = None 94*6777b538SAndroid Build Coastguard Worker i = 0 95*6777b538SAndroid Build Coastguard Worker remaining_args = [] 96*6777b538SAndroid Build Coastguard Worker while i < len(raw_args): 97*6777b538SAndroid Build Coastguard Worker if raw_args[i] == '--isolated-outdir' and i < len(raw_args)-1: 98*6777b538SAndroid Build Coastguard Worker outdir = raw_args[i+1] 99*6777b538SAndroid Build Coastguard Worker i += 2 100*6777b538SAndroid Build Coastguard Worker elif raw_args[i].startswith('--isolated-outdir='): 101*6777b538SAndroid Build Coastguard Worker outdir = raw_args[i][len('--isolated-outdir='):] 102*6777b538SAndroid Build Coastguard Worker i += 1 103*6777b538SAndroid Build Coastguard Worker else: 104*6777b538SAndroid Build Coastguard Worker remaining_args.append(raw_args[i]) 105*6777b538SAndroid Build Coastguard Worker i += 1 106*6777b538SAndroid Build Coastguard Worker if not outdir and 'ISOLATED_OUTDIR' in os.environ: 107*6777b538SAndroid Build Coastguard Worker outdir = os.environ['ISOLATED_OUTDIR'] 108*6777b538SAndroid Build Coastguard Worker return outdir, remaining_args 109*6777b538SAndroid Build Coastguard Worker 110*6777b538SAndroid Build Coastguard Worker def InsertWrapperScriptArgs(args): 111*6777b538SAndroid Build Coastguard Worker if '--wrapper-script-args' in args: 112*6777b538SAndroid Build Coastguard Worker idx = args.index('--wrapper-script-args') 113*6777b538SAndroid Build Coastguard Worker args.insert(idx + 1, shlex.join(sys.argv)) 114*6777b538SAndroid Build Coastguard Worker 115*6777b538SAndroid Build Coastguard Worker def FilterIsolatedOutdirBasedArgs(outdir, args): 116*6777b538SAndroid Build Coastguard Worker rargs = [] 117*6777b538SAndroid Build Coastguard Worker i = 0 118*6777b538SAndroid Build Coastguard Worker while i < len(args): 119*6777b538SAndroid Build Coastguard Worker if 'ISOLATED_OUTDIR' in args[i]: 120*6777b538SAndroid Build Coastguard Worker if outdir: 121*6777b538SAndroid Build Coastguard Worker # Rewrite the arg. 122*6777b538SAndroid Build Coastguard Worker rargs.append(args[i].replace('${{ISOLATED_OUTDIR}}', 123*6777b538SAndroid Build Coastguard Worker outdir).replace( 124*6777b538SAndroid Build Coastguard Worker '$ISOLATED_OUTDIR', outdir)) 125*6777b538SAndroid Build Coastguard Worker i += 1 126*6777b538SAndroid Build Coastguard Worker else: 127*6777b538SAndroid Build Coastguard Worker # Simply drop the arg. 128*6777b538SAndroid Build Coastguard Worker i += 1 129*6777b538SAndroid Build Coastguard Worker elif (not outdir and 130*6777b538SAndroid Build Coastguard Worker args[i].startswith('-') and 131*6777b538SAndroid Build Coastguard Worker '=' not in args[i] and 132*6777b538SAndroid Build Coastguard Worker i < len(args) - 1 and 133*6777b538SAndroid Build Coastguard Worker 'ISOLATED_OUTDIR' in args[i+1]): 134*6777b538SAndroid Build Coastguard Worker # Parsing this case is ambiguous; if we're given 135*6777b538SAndroid Build Coastguard Worker # `--foo $ISOLATED_OUTDIR` we can't tell if $ISOLATED_OUTDIR 136*6777b538SAndroid Build Coastguard Worker # is meant to be the value of foo, or if foo takes no argument 137*6777b538SAndroid Build Coastguard Worker # and $ISOLATED_OUTDIR is the first positional arg. 138*6777b538SAndroid Build Coastguard Worker # 139*6777b538SAndroid Build Coastguard Worker # We assume the former will be much more common, and so we 140*6777b538SAndroid Build Coastguard Worker # need to drop --foo and $ISOLATED_OUTDIR. 141*6777b538SAndroid Build Coastguard Worker i += 2 142*6777b538SAndroid Build Coastguard Worker else: 143*6777b538SAndroid Build Coastguard Worker rargs.append(args[i]) 144*6777b538SAndroid Build Coastguard Worker i += 1 145*6777b538SAndroid Build Coastguard Worker return rargs 146*6777b538SAndroid Build Coastguard Worker 147*6777b538SAndroid Build Coastguard Worker def ForwardSignals(proc): 148*6777b538SAndroid Build Coastguard Worker def _sig_handler(sig, _): 149*6777b538SAndroid Build Coastguard Worker if proc.poll() is not None: 150*6777b538SAndroid Build Coastguard Worker return 151*6777b538SAndroid Build Coastguard Worker # SIGBREAK is defined only for win32. 152*6777b538SAndroid Build Coastguard Worker # pylint: disable=no-member 153*6777b538SAndroid Build Coastguard Worker if sys.platform == 'win32' and sig == signal.SIGBREAK: 154*6777b538SAndroid Build Coastguard Worker print("Received signal(%d), sending CTRL_BREAK_EVENT to process %d" % (sig, proc.pid)) 155*6777b538SAndroid Build Coastguard Worker proc.send_signal(signal.CTRL_BREAK_EVENT) 156*6777b538SAndroid Build Coastguard Worker else: 157*6777b538SAndroid Build Coastguard Worker print("Forwarding signal(%d) to process %d" % (sig, proc.pid)) 158*6777b538SAndroid Build Coastguard Worker proc.send_signal(sig) 159*6777b538SAndroid Build Coastguard Worker # pylint: enable=no-member 160*6777b538SAndroid Build Coastguard Worker if sys.platform == 'win32': 161*6777b538SAndroid Build Coastguard Worker signal.signal(signal.SIGBREAK, _sig_handler) # pylint: disable=no-member 162*6777b538SAndroid Build Coastguard Worker else: 163*6777b538SAndroid Build Coastguard Worker signal.signal(signal.SIGTERM, _sig_handler) 164*6777b538SAndroid Build Coastguard Worker signal.signal(signal.SIGINT, _sig_handler) 165*6777b538SAndroid Build Coastguard Worker 166*6777b538SAndroid Build Coastguard Worker def Popen(*args, **kwargs): 167*6777b538SAndroid Build Coastguard Worker assert 'creationflags' not in kwargs 168*6777b538SAndroid Build Coastguard Worker if sys.platform == 'win32': 169*6777b538SAndroid Build Coastguard Worker # Necessary for signal handling. See crbug.com/733612#c6. 170*6777b538SAndroid Build Coastguard Worker kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP 171*6777b538SAndroid Build Coastguard Worker return subprocess.Popen(*args, **kwargs) 172*6777b538SAndroid Build Coastguard Worker 173*6777b538SAndroid Build Coastguard Worker def RunCommand(cmd): 174*6777b538SAndroid Build Coastguard Worker process = Popen(cmd) 175*6777b538SAndroid Build Coastguard Worker ForwardSignals(process) 176*6777b538SAndroid Build Coastguard Worker while process.poll() is None: 177*6777b538SAndroid Build Coastguard Worker time.sleep(0.1) 178*6777b538SAndroid Build Coastguard Worker return process.returncode 179*6777b538SAndroid Build Coastguard Worker 180*6777b538SAndroid Build Coastguard Worker 181*6777b538SAndroid Build Coastguard Worker def main(raw_args): 182*6777b538SAndroid Build Coastguard Worker executable_path = ExpandWrappedPath('{executable_path}') 183*6777b538SAndroid Build Coastguard Worker outdir, remaining_args = FindIsolatedOutdir(raw_args) 184*6777b538SAndroid Build Coastguard Worker args = {executable_args} 185*6777b538SAndroid Build Coastguard Worker InsertWrapperScriptArgs(args) 186*6777b538SAndroid Build Coastguard Worker args = FilterIsolatedOutdirBasedArgs(outdir, args) 187*6777b538SAndroid Build Coastguard Worker executable_args = ExpandWrappedPaths(args) 188*6777b538SAndroid Build Coastguard Worker cmd = [executable_path] + executable_args + remaining_args 189*6777b538SAndroid Build Coastguard Worker if executable_path.endswith('.py'): 190*6777b538SAndroid Build Coastguard Worker cmd = [sys.executable] + cmd 191*6777b538SAndroid Build Coastguard Worker return RunCommand(cmd) 192*6777b538SAndroid Build Coastguard Worker 193*6777b538SAndroid Build Coastguard Worker 194*6777b538SAndroid Build Coastguard Worker if __name__ == '__main__': 195*6777b538SAndroid Build Coastguard Worker sys.exit(main(sys.argv[1:])) 196*6777b538SAndroid Build Coastguard Worker """) 197*6777b538SAndroid Build Coastguard Worker 198*6777b538SAndroid Build Coastguard Worker 199*6777b538SAndroid Build Coastguard Workerdef Wrap(args): 200*6777b538SAndroid Build Coastguard Worker """Writes a wrapped script according to the provided arguments. 201*6777b538SAndroid Build Coastguard Worker 202*6777b538SAndroid Build Coastguard Worker Arguments: 203*6777b538SAndroid Build Coastguard Worker args: an argparse.Namespace object containing command-line arguments 204*6777b538SAndroid Build Coastguard Worker as parsed by a parser returned by CreateArgumentParser. 205*6777b538SAndroid Build Coastguard Worker """ 206*6777b538SAndroid Build Coastguard Worker path_to_output_dir = os.path.relpath( 207*6777b538SAndroid Build Coastguard Worker args.output_directory, 208*6777b538SAndroid Build Coastguard Worker os.path.dirname(args.wrapper_script)) 209*6777b538SAndroid Build Coastguard Worker 210*6777b538SAndroid Build Coastguard Worker with open(args.wrapper_script, 'w') as wrapper_script: 211*6777b538SAndroid Build Coastguard Worker py_contents = PY_TEMPLATE.format( 212*6777b538SAndroid Build Coastguard Worker path_to_output_dir=path_to_output_dir, 213*6777b538SAndroid Build Coastguard Worker executable_path=str(args.executable), 214*6777b538SAndroid Build Coastguard Worker executable_args=str(args.executable_args)) 215*6777b538SAndroid Build Coastguard Worker template = SCRIPT_TEMPLATES[args.script_language] 216*6777b538SAndroid Build Coastguard Worker wrapper_script.write(template.format(script=py_contents)) 217*6777b538SAndroid Build Coastguard Worker os.chmod(args.wrapper_script, 0o750) 218*6777b538SAndroid Build Coastguard Worker 219*6777b538SAndroid Build Coastguard Worker return 0 220*6777b538SAndroid Build Coastguard Worker 221*6777b538SAndroid Build Coastguard Worker 222*6777b538SAndroid Build Coastguard Workerdef CreateArgumentParser(): 223*6777b538SAndroid Build Coastguard Worker """Creates an argparse.ArgumentParser instance.""" 224*6777b538SAndroid Build Coastguard Worker parser = argparse.ArgumentParser() 225*6777b538SAndroid Build Coastguard Worker parser.add_argument( 226*6777b538SAndroid Build Coastguard Worker '--executable', 227*6777b538SAndroid Build Coastguard Worker help='Executable to wrap.') 228*6777b538SAndroid Build Coastguard Worker parser.add_argument( 229*6777b538SAndroid Build Coastguard Worker '--wrapper-script', 230*6777b538SAndroid Build Coastguard Worker help='Path to which the wrapper script will be written.') 231*6777b538SAndroid Build Coastguard Worker parser.add_argument( 232*6777b538SAndroid Build Coastguard Worker '--output-directory', 233*6777b538SAndroid Build Coastguard Worker help='Path to the output directory.') 234*6777b538SAndroid Build Coastguard Worker parser.add_argument( 235*6777b538SAndroid Build Coastguard Worker '--script-language', 236*6777b538SAndroid Build Coastguard Worker choices=SCRIPT_TEMPLATES.keys(), 237*6777b538SAndroid Build Coastguard Worker help='Language in which the wrapper script will be written.') 238*6777b538SAndroid Build Coastguard Worker parser.add_argument( 239*6777b538SAndroid Build Coastguard Worker 'executable_args', nargs='*', 240*6777b538SAndroid Build Coastguard Worker help='Arguments to wrap into the executable.') 241*6777b538SAndroid Build Coastguard Worker return parser 242*6777b538SAndroid Build Coastguard Worker 243*6777b538SAndroid Build Coastguard Worker 244*6777b538SAndroid Build Coastguard Workerdef main(raw_args): 245*6777b538SAndroid Build Coastguard Worker parser = CreateArgumentParser() 246*6777b538SAndroid Build Coastguard Worker args = parser.parse_args(raw_args) 247*6777b538SAndroid Build Coastguard Worker return Wrap(args) 248*6777b538SAndroid Build Coastguard Worker 249*6777b538SAndroid Build Coastguard Worker 250*6777b538SAndroid Build Coastguard Workerif __name__ == '__main__': 251*6777b538SAndroid Build Coastguard Worker sys.exit(main(sys.argv[1:])) 252