• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14
15import("//build_overrides/pigweed.gni")
16
17import("$dir_pw_build/python_gn_args.gni")
18
19# Defines an action that runs a Python script.
20#
21# This wraps a regular Python script GN action with an invocation of a script-
22# runner script that adds useful features. pw_python_action() uses the same
23# actions as GN's action(), with the following additions or changes:
24#
25#   module          May be used in place of the script argument to run the
26#                   provided Python module with `python -m` instead of a script.
27#                   Either script or module must be provided.
28#
29#   capture_output  If true, script output is hidden unless the script fails
30#                   with an error. Defaults to true.
31#
32#   stamp           File to touch if the script is successful. Actions that
33#                   don't create output files can use this stamp file instead of
34#                   creating their own placeholder file. If true, a generic file
35#                   is used. If false or not set, no file is touched.
36#
37#   environment     Environment variables to set, passed as a list of NAME=VALUE
38#                   strings.
39#
40#   args            Same as the standard action args, except special expressions
41#                   may be used to extract information not normally accessible
42#                   in GN. These include the following:
43#
44#                     <TARGET_FILE(//some/label:here)> - expands to the
45#                         output file (such as a .a or .elf) from a GN target
46#                     <TARGET_FILE_IF_EXISTS(//some/label:here)> - expands to
47#                         the output file if the target exists, or nothing
48#                     <TARGET_OBJECTS(//some/label:here)> - expands to the
49#                         object files produced by the provided GN target
50#
51#   python_deps     Dependencies on pw_python_package or related Python targets.
52#
53#   working_directory  Switch to the provided working directory before running
54#                      the Python script or action.
55#
56#   command_launcher   Arguments to prepend to the Python command, e.g.
57#                      '/usr/bin/fakeroot --' to run the Python script within a
58#                      fakeroot environment.
59#
60#   venv            Optional gn target of the pw_python_venv that should be used
61#                   to run this action.
62#
63template("pw_python_action") {
64  assert(defined(invoker.script) != defined(invoker.module),
65         "pw_python_action requires either 'script' or 'module'")
66
67  _script_args = [
68    # GN root directory relative to the build directory (in which the runner
69    # script is invoked).
70    "--gn-root",
71    rebase_path("//", root_build_dir),
72
73    # Current directory, used to resolve relative paths.
74    "--current-path",
75    rebase_path(".", root_build_dir),
76
77    "--default-toolchain=$default_toolchain",
78    "--current-toolchain=$current_toolchain",
79  ]
80
81  _use_build_dir_virtualenv = true
82
83  if (defined(invoker.environment)) {
84    foreach(variable, invoker.environment) {
85      _script_args += [ "--env=$variable" ]
86    }
87  }
88
89  if (defined(invoker.inputs)) {
90    _inputs = invoker.inputs
91  } else {
92    _inputs = []
93  }
94
95  # List the script to run as an input so that the action is re-run when it is
96  # modified.
97  if (defined(invoker.script)) {
98    _inputs += [ invoker.script ]
99  }
100
101  if (defined(invoker.outputs)) {
102    _outputs = invoker.outputs
103  } else {
104    _outputs = []
105  }
106
107  # If a stamp file is requested, add it as an output of the runner script.
108  if (defined(invoker.stamp) && invoker.stamp != false) {
109    if (invoker.stamp == true) {
110      _stamp_file = "$target_gen_dir/$target_name.pw_pystamp"
111    } else {
112      _stamp_file = invoker.stamp
113    }
114
115    _outputs += [ _stamp_file ]
116    _script_args += [
117      "--touch",
118      rebase_path(_stamp_file, root_build_dir),
119    ]
120  }
121
122  # Capture output or not (defaults to true).
123  if (!defined(invoker.capture_output) || invoker.capture_output) {
124    _script_args += [ "--capture-output" ]
125  }
126
127  if (defined(invoker.module)) {
128    _script_args += [
129      "--module",
130      invoker.module,
131    ]
132
133    # Pip installs should only ever need to occur in the Pigweed
134    # environment. For these actions do not use the build_dir virtualenv.
135    if (invoker.module == "pip") {
136      _use_build_dir_virtualenv = false
137    }
138  }
139
140  # Override to force using or not using the venv.
141  if (defined(invoker._pw_internal_run_in_venv)) {
142    _use_build_dir_virtualenv = invoker._pw_internal_run_in_venv
143  }
144
145  if (defined(invoker.working_directory)) {
146    _script_args += [
147      "--working-directory",
148      invoker.working_directory,
149    ]
150  }
151
152  if (defined(invoker.command_launcher)) {
153    _script_args += [
154      "--command-launcher",
155      invoker.command_launcher,
156    ]
157  }
158
159  if (defined(invoker._pw_action_type)) {
160    _action_type = invoker._pw_action_type
161  } else {
162    _action_type = "action"
163  }
164
165  if (defined(invoker.deps)) {
166    _deps = invoker.deps
167  } else {
168    _deps = []
169  }
170
171  _py_metadata_deps = []
172
173  if (defined(invoker.python_deps)) {
174    foreach(dep, invoker.python_deps) {
175      _deps += [ get_label_info(dep, "label_no_toolchain") + ".install(" +
176                 get_label_info(dep, "toolchain") + ")" ]
177
178      # Ensure each python_dep is added to the PYTHONPATH by depinding on the
179      # ._package_metadata subtarget.
180      _py_metadata_deps += [ get_label_info(dep, "label_no_toolchain") +
181                             "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
182    }
183
184    # Add the base target as a dep so the action reruns when any source files
185    # change, even if the package does not have to be reinstalled.
186    _deps += invoker.python_deps
187    _deps += _py_metadata_deps
188  }
189
190  # Check for additional PYTHONPATH dependencies.
191  _extra_python_metadata_deps = []
192  if (defined(invoker.python_metadata_deps)) {
193    foreach(dep, invoker.python_metadata_deps) {
194      _extra_python_metadata_deps +=
195          [ get_label_info(dep, "label_no_toolchain") +
196            "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
197    }
198  }
199
200  _metadata_path_list_file =
201      "${target_gen_dir}/${target_name}_metadata_path_list.txt"
202
203  # GN metadata only dependencies used for setting PYTHONPATH.
204  _metadata_deps = _py_metadata_deps + _extra_python_metadata_deps
205
206  # Build a list of relative paths containing all the python
207  # package_metadata.json files we depend on.
208  _metadata_path_list_target = "${target_name}._metadata_path_list.txt"
209  generated_file(_metadata_path_list_target) {
210    data_keys = [ "pw_python_package_metadata_json" ]
211    rebase = root_build_dir
212    deps = _metadata_deps
213    outputs = [ _metadata_path_list_file ]
214  }
215  _deps += [ ":${_metadata_path_list_target}" ]
216
217  # Set venv options if needed.
218  if (_use_build_dir_virtualenv) {
219    _venv_target_label = pw_build_PYTHON_BUILD_VENV
220    if (defined(invoker.venv)) {
221      _venv_target_label = invoker.venv
222    }
223    _venv_target_label =
224        get_label_info(_venv_target_label, "label_no_toolchain") +
225        "($pw_build_PYTHON_TOOLCHAIN)"
226
227    _venv_json =
228        get_label_info(_venv_target_label, "target_gen_dir") + "/" +
229        get_label_info(_venv_target_label, "name") + "/venv_metadata.json"
230    _script_args += [
231      "--python-virtualenv-config",
232      rebase_path(_venv_json, root_build_dir),
233    ]
234  }
235  _script_args += [
236    "--python-dep-list-files",
237    rebase_path(_metadata_path_list_file, root_build_dir),
238  ]
239
240  # "--" indicates the end of arguments to the runner script.
241  # Everything beyond this point is interpreted as the command and arguments
242  # of the Python script to run.
243  _script_args += [ "--" ]
244
245  if (defined(invoker.script)) {
246    _script_args += [ rebase_path(invoker.script, root_build_dir) ]
247  }
248
249  _forward_python_metadata_deps = false
250  if (defined(invoker._forward_python_metadata_deps)) {
251    _forward_python_metadata_deps = true
252  }
253  if (_forward_python_metadata_deps) {
254    _script_args += [
255      "--python-dep-list-files",
256      rebase_path(_metadata_path_list_file, root_build_dir),
257    ]
258  }
259
260  if (defined(invoker.args)) {
261    _script_args += invoker.args
262  }
263
264  # Assume third party PyPI deps should be available in the build_dir virtualenv.
265  _install_venv_3p_deps = true
266  if (!_use_build_dir_virtualenv ||
267      (defined(invoker._skip_installing_external_python_deps) &&
268       invoker._skip_installing_external_python_deps)) {
269    _install_venv_3p_deps = false
270  }
271
272  # Check that script or module is a present and not a no-op.
273  _run_script_or_module = false
274  if (defined(invoker.script) || defined(invoker.module)) {
275    _run_script_or_module = true
276  }
277
278  target(_action_type, target_name) {
279    _ignore_vars = [
280      "script",
281      "args",
282      "deps",
283      "inputs",
284      "outputs",
285    ]
286    forward_variables_from(invoker, "*", _ignore_vars)
287
288    script = "$dir_pw_build/py/pw_build/python_runner.py"
289    args = _script_args
290    inputs = _inputs
291    outputs = _outputs
292    deps = _deps
293
294    if (_install_venv_3p_deps && _run_script_or_module) {
295      deps += [ get_label_info(_venv_target_label, "label_no_toolchain") +
296                "._install_3p_deps($pw_build_PYTHON_TOOLCHAIN)" ]
297    }
298  }
299}
300
301# Runs pw_python_action once per file over a set of sources.
302#
303# This template brings pw_python_action's features to action_foreach. Usage is
304# the same as pw_python_action, except that sources must be provided and source
305# expansion (e.g. "{{source}}") may be used in args and outputs.
306#
307# See the pw_python_action and action_foreach documentation for full details.
308template("pw_python_action_foreach") {
309  assert(defined(invoker.sources) && invoker.sources != [],
310         "pw_python_action_foreach requires a list of one or more sources")
311
312  pw_python_action(target_name) {
313    if (defined(invoker.stamp) && invoker.stamp != false) {
314      if (invoker.stamp == true) {
315        # Use source file names in the generated stamp file path so they are
316        # unique for each source.
317        stamp = "$target_gen_dir/{{source_file_part}}.pw_pystamp"
318      } else {
319        stamp = invoker.stamp
320      }
321    } else {
322      stamp = false
323    }
324
325    forward_variables_from(invoker, "*", [ "stamp" ])
326
327    _pw_action_type = "action_foreach"
328  }
329}
330