Add a stamp option to pw_python_script
GN requires all actions to have at least one output file. However, many
Python scripts are run to process data without generating a file. Such
scripts would previously have to specify a dummy output file and touch
it on completion.
This change adds this stamp functionality to the script-runner script.
The pw_python_script template accepts a boolean "stamp" argument. If
set, the runner will touch a dummy file after it runs its sub-command.
Change-Id: Iff0408ea0d93007418a1766f1eb7ed7b30ec6913
diff --git a/pw_build/py/nop.py b/pw_build/py/nop.py
new file mode 100644
index 0000000..aaa4e8a
--- /dev/null
+++ b/pw_build/py/nop.py
@@ -0,0 +1,27 @@
+# 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.
+
+"""Does nothing.
+
+The purpose of this script is to allow for source file dependencies within GN
+to be attached to targets that do not typically support them, such as groups.
+
+For example, instead of creating a group target, a pw_python_script target to
+run this script can be created. The script can be given a list of input files,
+causing GN to rebuild the target and everything that depends on it whenever any
+input file is modified.
+
+This is useful in the case where metadata is attached to a group of files but
+not collected into a generated_file until a later target.
+"""
diff --git a/pw_build/py/python_runner.py b/pw_build/py/python_runner.py
index 5db452d..49396a5 100644
--- a/pw_build/py/python_runner.py
+++ b/pw_build/py/python_runner.py
@@ -16,11 +16,10 @@
import argparse
import os
+import pathlib
import subprocess
import sys
-from typing import List
-
def parse_args() -> argparse.Namespace:
"""Parses arguments for this script, splitting out the command to run."""
@@ -30,6 +29,8 @@
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()
@@ -51,8 +52,7 @@
target_path, target_name = target.split(':')
- extensions = ['', '.elf']
- for extension in extensions:
+ for extension in ['', '.elf', '.exe']:
potential_filename = f'{target_path}/{target_name}{extension}'
if os.path.isfile(potential_filename):
return potential_filename
@@ -61,27 +61,28 @@
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.
+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.
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.
"""
- for i, arg in enumerate(command):
- if not arg.startswith('//'):
- continue
+ if not string.startswith('//'):
+ return string
- resolved_path = gn_root + arg[2:]
+ resolved_path = gn_root + string[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
+ # 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 main() -> int:
@@ -92,15 +93,30 @@
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)
+ resolved_command = [resolve_path(
+ args.gn_root, args.out_dir, arg) for arg in args.command[1:]]
except FileNotFoundError as err:
- print(f'{sys.argv[0]} {err}', file=sys.stderr)
+ print(f'{sys.argv[0]}: {err}', file=sys.stderr)
return 1
- return subprocess.call(command)
+ command = [sys.executable] + resolved_command
+ print('RUN', ' '.join(command), flush=True)
+
+ try:
+ status = subprocess.call(command)
+ except subprocess.CalledProcessError as err:
+ print(f'{sys.argv[0]}: {err}', file=sys.stderr)
+ 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)
+ print('TOUCH', touch_file)
+ pathlib.Path(touch_file).touch()
+
+ return status
if __name__ == '__main__':
diff --git a/pw_build/python_script.gni b/pw_build/python_script.gni
index 1fd340a..ced7a10 100644
--- a/pw_build/python_script.gni
+++ b/pw_build/python_script.gni
@@ -39,6 +39,11 @@
# assumes that the target refers to a file built by Ninja and tries to
# locate it within the output directory.
#
+# Additionally, this template can accept a boolean "stamp" argument. If set to
+# true, the script runner will touch a file to indicate the success of the run.
+# This is provided so that individual Python scripts are not required to define
+# an output file if they do not have one.
+#
# Path resolution examples (assuming the build directory is //out):
#
# BEFORE AFTER
@@ -62,13 +67,29 @@
# Output directory root, used to determine whether to search for a binary.
"--out-dir",
root_out_dir,
-
- # "--" indicates the end of arguments to the runner script.
- # Everything beyond this point is interpreted as the command and arguments
- # of the Python script to run.
- "--",
]
+ if (defined(invoker.outputs)) {
+ _outputs = invoker.outputs
+ } else {
+ _outputs = []
+ }
+
+ # If a stamp file is requested, add it as an output of the runner script.
+ if (defined(invoker.stamp) && invoker.stamp) {
+ _stamp_file = "$target_gen_dir/$target_name.pw_pystamp"
+ _outputs += [ _stamp_file ]
+ _script_args += [
+ "--touch",
+ _stamp_file,
+ ]
+ }
+
+ # "--" indicates the end of arguments to the runner script.
+ # Everything beyond this point is interpreted as the command and arguments
+ # of the Python script to run.
+ _script_args += [ "--" ]
+
_script_args += [ get_path_info(invoker.script, "abspath") ]
if (defined(invoker.args)) {
_script_args += invoker.args
@@ -78,10 +99,12 @@
_ignore_vars = [
"script",
"args",
+ "outputs",
]
forward_variables_from(invoker, "*", _ignore_vars)
script = "$dir_pw_build/py/python_runner.py"
args = _script_args
+ outputs = _outputs
}
}
diff --git a/pw_unit_test/py/test_runner.py b/pw_unit_test/py/test_runner.py
index 9bbe150..019a8ce 100644
--- a/pw_unit_test/py/test_runner.py
+++ b/pw_unit_test/py/test_runner.py
@@ -29,8 +29,6 @@
"""Parses command-line arguments."""
parser = argparse.ArgumentParser('Run Pigweed unit tests')
- parser.add_argument('--touch', type=str,
- help='File to touch after test run')
parser.add_argument('test', type=str, help='Path to unit test binary')
return parser.parse_args()
@@ -46,13 +44,6 @@
print(f'{sys.argv[0]}: {err}', file=sys.stderr)
return 1
- # GN expects "action" targets to output a file, and uses that to determine
- # whether the target should be run again. Touching an empty file allows GN
- # to only run unit tests which have been affected by code changes since the
- # previous run, taking advantage of its dependency resolution.
- if args.touch is not None:
- pathlib.Path(args.touch).touch()
-
return exit_status
diff --git a/pw_unit_test/test.gni b/pw_unit_test/test.gni
index 8e3d27c..2a8629a 100644
--- a/pw_unit_test/test.gni
+++ b/pw_unit_test/test.gni
@@ -84,22 +84,13 @@
# executable using the test runner script.
_run_action_name = _test_target_name + "_run"
- # The runner script touches this file to indicate that the test has run.
- _stamp_file = "$target_gen_dir/${_test_target_name}.test_completion.stamp"
-
pw_python_script(_run_action_name) {
deps = [
":$_test_target_name",
]
script = "$dir_pw_unit_test/py/test_runner.py"
- args = [
- "--touch",
- get_path_info(_stamp_file, "abspath"),
- get_path_info("$target_out_dir:$_test_target_name", "abspath"),
- ]
- outputs = [
- _stamp_file,
- ]
+ args = [ get_path_info("$target_out_dir:$_test_target_name", "abspath") ]
+ stamp = true
}
}
}