blob: 971775e93bf0d4382d0cc945d87166e92bc6730b [file] [log] [blame]
# Copyright 2019 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Script that preprocesses a Python command then runs it."""
import argparse
import logging
import os
import pathlib
import shlex
import subprocess
import sys
_LOG = logging.getLogger(__name__)
def parse_args() -> argparse.Namespace:
"""Parses arguments for this script, splitting out the command to run."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--gn-root',
type=str,
required=True,
help='Path to the root of the GN tree')
parser.add_argument('--out-dir',
type=str,
required=True,
help='Path to the GN build output directory')
parser.add_argument('--touch',
type=str,
help='File to touch after command is run')
parser.add_argument('command',
nargs=argparse.REMAINDER,
help='Python script with arguments to run')
return parser.parse_args()
def find_binary(target: str) -> str:
"""Tries to find a binary for a gn build target.
Args:
target: Relative filesystem path to the target's output directory and
target name, separated by a colon.
Returns:
Full path to the target's binary.
Raises:
RuntimeError: No binary found for target.
"""
target_path, target_name = target.split(':')
for extension in ['', '.elf', '.exe']:
potential_filename = f'{target_path}/{target_name}{extension}'
if os.path.isfile(potential_filename):
return potential_filename
raise FileNotFoundError(
f'Could not find output binary for build target {target}')
def _resolve_path(gn_root: str, out_dir: str, string: str) -> str:
"""Resolves a string to a filesystem path if it is a GN path."""
if not string.startswith('//'):
return string
resolved_path = gn_root + string[2:]
# GN targets have the format '/path/to/directory:target_name'.
if string.startswith(out_dir) and ':' in string:
return find_binary(resolved_path)
return resolved_path
def resolve_path(gn_root: str, out_dir: str, string: str) -> str:
"""Resolves GN paths to filesystem paths in a semicolon-separated string.
GN paths are assumed to be absolute, starting with "//". This is replaced
with the relative filesystem path of the GN root directory.
If the string is not a GN path, it is returned unmodified.
If a path refers to the GN output directory and a target name is defined,
attempts to locate a binary file for the target within the out directory.
"""
return ';'.join(
_resolve_path(gn_root, out_dir, path) for path in string.split(';'))
def main() -> int:
"""Script entry point."""
args = parse_args()
if not args.command or args.command[0] != '--':
_LOG.error(f'{sys.argv[0]} requires a command to run')
return 1
try:
resolved_command = [
resolve_path(args.gn_root, args.out_dir, arg)
for arg in args.command[1:]
]
except FileNotFoundError as err:
_LOG.error(f'{sys.argv[0]}: {err}')
return 1
command = [sys.executable] + resolved_command
_LOG.debug('RUN ' + ' '.join(shlex.quote(arg) for arg in command))
# Add PW_SUBPROCESS to suppress unnecessary extra log headers; supported by
# most PW subcommands.
new_env = os.environ.copy()
new_env['PW_SUBPROCESS'] = '1'
try:
status = subprocess.call(command, env=new_env)
except subprocess.CalledProcessError as err:
_LOG.error(f'{sys.argv[0]}: {err}')
return 1
if status == 0 and args.touch:
# If a touch file is provided, touch it to indicate a successful run of
# the command.
touch_file = resolve_path(args.gn_root, args.out_dir, args.touch)
_LOG.debug('TOUCH %s', touch_file)
pathlib.Path(touch_file).touch()
return status
if __name__ == '__main__':
sys.exit(main())