third_party.pigweed.src/pw_build/py/python_runner.py
Alexei Frolov d1f98fadeb Script-runner script and pw_python_script template
This change adds a pw_python_script GN template which defines an action
to run a Python script through a script-runner script. This runner is
responsible for resolving any GN paths to filesystem paths, and finding
output binaries for compiled targets. This allows writing Python scripts
which are ignorant of the GN build system and work only with filesystem
paths.

The unit test runner script is updated to use the new runner template.

Change-Id: I132bb620af2bb1e57e9278fac57b676f8ab5a415
2019-11-12 18:40:24 +00:00

108 lines
3.3 KiB
Python

# 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 os
import subprocess
import sys
from typing import List
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('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(':')
extensions = ['', '.elf']
for extension in extensions:
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_paths(gn_root: str, out_dir: str, command: List[str]) -> None:
"""Runs through an argument list, replacing GN paths with filesystem paths.
GN paths are assumed to be absolute, starting with "//". This is replaced
with the relative filesystem path of the GN root directory.
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.
"""
for i, arg in enumerate(command):
if not arg.startswith('//'):
continue
resolved_path = gn_root + arg[2:]
# GN targets have the format '/path/to/directory:target_name'.
if arg.startswith(out_dir) and ':' in arg:
command[i] = find_binary(resolved_path)
else:
command[i] = resolved_path
def main() -> int:
"""Script entry point."""
args = parse_args()
if not args.command or args.command[0] != '--':
print(f'{sys.argv[0]} requires a command to run', file=sys.stderr)
return 1
command = [sys.executable] + args.command[1:]
try:
resolve_paths(args.gn_root, args.out_dir, command)
except FileNotFoundError as err:
print(f'{sys.argv[0]} {err}', file=sys.stderr)
return 1
return subprocess.call(command)
if __name__ == '__main__':
sys.exit(main())