blob: b45f5f3bf9af60802b69f880b3dbebc7068b08b4 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Wraps an executable and any provided arguments into an executable script."""
import argparse
import os
import sys
import textwrap
# The bash template passes the python script into vpython via stdin.
# The interpreter doesn't know about the script, so we have bash
# inject the script location.
BASH_TEMPLATE = textwrap.dedent("""\
#!/usr/bin/env vpython3
_SCRIPT_LOCATION = __file__
{script}
""")
# The batch template reruns the batch script with vpython, with the -x
# flag instructing the interpreter to ignore the first line. The interpreter
# knows about the (batch) script in this case, so it can get the file location
# directly.
BATCH_TEMPLATE = textwrap.dedent("""\
@SETLOCAL ENABLEDELAYEDEXPANSION \
& vpython3.bat -x "%~f0" %* \
& EXIT /B !ERRORLEVEL!
_SCRIPT_LOCATION = __file__
{script}
""")
SCRIPT_TEMPLATES = {
'bash': BASH_TEMPLATE,
'batch': BATCH_TEMPLATE,
}
PY_TEMPLATE = textwrap.dedent("""\
import os
import re
import shlex
import signal
import subprocess
import sys
import time
_WRAPPED_PATH_RE = re.compile(r'@WrappedPath\(([^)]+)\)')
_PATH_TO_OUTPUT_DIR = '{path_to_output_dir}'
_SCRIPT_DIR = os.path.dirname(os.path.realpath(_SCRIPT_LOCATION))
def ExpandWrappedPath(arg):
m = _WRAPPED_PATH_RE.match(arg)
if m:
relpath = os.path.join(
os.path.relpath(_SCRIPT_DIR), _PATH_TO_OUTPUT_DIR, m.group(1))
npath = os.path.normpath(relpath)
if os.path.sep not in npath:
# If the original path points to something in the current directory,
# returning the normalized version of it can be a problem.
# normpath() strips off the './' part of the path
# ('./foo' becomes 'foo'), which can be a problem if the result
# is passed to something like os.execvp(); in that case
# osexecvp() will search $PATH for the executable, rather than
# just execing the arg directly, and if '.' isn't in $PATH, this
# results in an error.
#
# So, we need to explicitly return './foo' (or '.\\foo' on windows)
# instead of 'foo'.
#
# Hopefully there are no cases where this causes a problem; if
# there are, we will either need to change the interface to
# WrappedPath() somehow to distinguish between the two, or
# somehow ensure that the wrapped executable doesn't hit cases
# like this.
return '.' + os.path.sep + npath
return npath
return arg
def ExpandWrappedPaths(args):
for i, arg in enumerate(args):
args[i] = ExpandWrappedPath(arg)
return args
def FindIsolatedOutdir(raw_args):
outdir = None
i = 0
remaining_args = []
while i < len(raw_args):
if raw_args[i] == '--isolated-outdir' and i < len(raw_args)-1:
outdir = raw_args[i+1]
i += 2
elif raw_args[i].startswith('--isolated-outdir='):
outdir = raw_args[i][len('--isolated-outdir='):]
i += 1
else:
remaining_args.append(raw_args[i])
i += 1
if not outdir and 'ISOLATED_OUTDIR' in os.environ:
outdir = os.environ['ISOLATED_OUTDIR']
return outdir, remaining_args
def InsertWrapperScriptArgs(args):
if '--wrapper-script-args' in args:
idx = args.index('--wrapper-script-args')
args.insert(idx + 1, shlex.join(sys.argv))
def FilterIsolatedOutdirBasedArgs(outdir, args):
rargs = []
i = 0
while i < len(args):
if 'ISOLATED_OUTDIR' in args[i]:
if outdir:
# Rewrite the arg.
rargs.append(args[i].replace('${{ISOLATED_OUTDIR}}',
outdir).replace(
'$ISOLATED_OUTDIR', outdir))
i += 1
else:
# Simply drop the arg.
i += 1
elif (not outdir and
args[i].startswith('-') and
'=' not in args[i] and
i < len(args) - 1 and
'ISOLATED_OUTDIR' in args[i+1]):
# Parsing this case is ambiguous; if we're given
# `--foo $ISOLATED_OUTDIR` we can't tell if $ISOLATED_OUTDIR
# is meant to be the value of foo, or if foo takes no argument
# and $ISOLATED_OUTDIR is the first positional arg.
#
# We assume the former will be much more common, and so we
# need to drop --foo and $ISOLATED_OUTDIR.
i += 2
else:
rargs.append(args[i])
i += 1
return rargs
def ForwardSignals(proc):
def _sig_handler(sig, _):
if proc.poll() is not None:
return
# SIGBREAK is defined only for win32.
# pylint: disable=no-member
if sys.platform == 'win32' and sig == signal.SIGBREAK:
print("Received signal(%d), sending CTRL_BREAK_EVENT to process %d" % (sig, proc.pid))
proc.send_signal(signal.CTRL_BREAK_EVENT)
else:
print("Forwarding signal(%d) to process %d" % (sig, proc.pid))
proc.send_signal(sig)
# pylint: enable=no-member
if sys.platform == 'win32':
signal.signal(signal.SIGBREAK, _sig_handler) # pylint: disable=no-member
else:
signal.signal(signal.SIGTERM, _sig_handler)
signal.signal(signal.SIGINT, _sig_handler)
def Popen(*args, **kwargs):
assert 'creationflags' not in kwargs
if sys.platform == 'win32':
# Necessary for signal handling. See crbug.com/733612#c6.
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
return subprocess.Popen(*args, **kwargs)
def RunCommand(cmd):
process = Popen(cmd)
ForwardSignals(process)
while process.poll() is None:
time.sleep(0.1)
return process.returncode
def main(raw_args):
executable_path = ExpandWrappedPath('{executable_path}')
outdir, remaining_args = FindIsolatedOutdir(raw_args)
args = {executable_args}
InsertWrapperScriptArgs(args)
args = FilterIsolatedOutdirBasedArgs(outdir, args)
executable_args = ExpandWrappedPaths(args)
cmd = [executable_path] + executable_args + remaining_args
if executable_path.endswith('.py'):
cmd = [sys.executable] + cmd
return RunCommand(cmd)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
""")
def Wrap(args):
"""Writes a wrapped script according to the provided arguments.
Arguments:
args: an argparse.Namespace object containing command-line arguments
as parsed by a parser returned by CreateArgumentParser.
"""
path_to_output_dir = os.path.relpath(
args.output_directory,
os.path.dirname(args.wrapper_script))
with open(args.wrapper_script, 'w') as wrapper_script:
py_contents = PY_TEMPLATE.format(
path_to_output_dir=path_to_output_dir,
executable_path=str(args.executable),
executable_args=str(args.executable_args))
template = SCRIPT_TEMPLATES[args.script_language]
wrapper_script.write(template.format(script=py_contents))
os.chmod(args.wrapper_script, 0o750)
return 0
def CreateArgumentParser():
"""Creates an argparse.ArgumentParser instance."""
parser = argparse.ArgumentParser()
parser.add_argument(
'--executable',
help='Executable to wrap.')
parser.add_argument(
'--wrapper-script',
help='Path to which the wrapper script will be written.')
parser.add_argument(
'--output-directory',
help='Path to the output directory.')
parser.add_argument(
'--script-language',
choices=SCRIPT_TEMPLATES.keys(),
help='Language in which the wrapper script will be written.')
parser.add_argument(
'executable_args', nargs='*',
help='Arguments to wrap into the executable.')
return parser
def main(raw_args):
parser = CreateArgumentParser()
args = parser.parse_args(raw_args)
return Wrap(args)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))