| #!/usr/bin/env vpython | |
| # Copyright 2019 The Chromium Authors. All rights reserved. | |
| # 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 {vpython} | |
| _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 \ | |
| & {vpython}.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 subprocess | |
| import sys | |
| _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 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 main(raw_args): | |
| executable_path = ExpandWrappedPath('{executable_path}') | |
| outdir, remaining_args = FindIsolatedOutdir(raw_args) | |
| args = {executable_args} | |
| args = FilterIsolatedOutdirBasedArgs(outdir, args) | |
| executable_args = ExpandWrappedPaths(args) | |
| cmd = [executable_path] + args + remaining_args | |
| if executable_path.endswith('.py'): | |
| cmd = [sys.executable] + cmd | |
| return subprocess.call(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, vpython=args.vpython)) | |
| 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('--use-vpython3', | |
| dest='vpython', | |
| action='store_const', | |
| const='vpython3', | |
| default='vpython', | |
| help='Use vpython3 instead of vpython') | |
| 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:])) |