xref: /aosp_15_r20/external/pigweed/pw_build/python_action.gni (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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#   python_metadata_deps  Python-related dependencies that are only used as deps
54#                         for generating Python package metadata list, not the
55#                         overall Python script action. This should rarely be
56#                         used by non-Pigweed code.
57#
58#   working_directory  Switch to the provided working directory before running
59#                      the Python script or action.
60#
61#   command_launcher   Arguments to prepend to the Python command, e.g.
62#                      '/usr/bin/fakeroot --' to run the Python script within a
63#                      fakeroot environment.
64#
65#   venv            Optional gn target of the pw_python_venv that should be used
66#                   to run this action.
67#
68template("pw_python_action") {
69  assert(defined(invoker.script) != defined(invoker.module),
70         "pw_python_action requires either 'script' or 'module'")
71
72  _script_args = [
73    # GN root directory relative to the build directory (in which the runner
74    # script is invoked).
75    "--gn-root",
76    rebase_path("//", root_build_dir),
77
78    # Current directory, used to resolve relative paths.
79    "--current-path",
80    rebase_path(".", root_build_dir),
81
82    "--default-toolchain=$default_toolchain",
83    "--current-toolchain=$current_toolchain",
84  ]
85
86  _use_build_dir_virtualenv = true
87
88  if (defined(invoker.environment)) {
89    foreach(variable, invoker.environment) {
90      _script_args += [ "--env=$variable" ]
91    }
92  }
93
94  if (defined(invoker.inputs)) {
95    _inputs = invoker.inputs
96  } else {
97    _inputs = []
98  }
99
100  # List the script to run as an input so that the action is re-run when it is
101  # modified.
102  if (defined(invoker.script)) {
103    _inputs += [ invoker.script ]
104  }
105
106  if (defined(invoker.outputs)) {
107    _outputs = invoker.outputs
108  } else {
109    _outputs = []
110  }
111
112  # If a stamp file is requested, add it as an output of the runner script.
113  if (defined(invoker.stamp) && invoker.stamp != false) {
114    if (invoker.stamp == true) {
115      _stamp_file = "$target_gen_dir/$target_name.pw_pystamp"
116    } else {
117      _stamp_file = invoker.stamp
118    }
119
120    _outputs += [ _stamp_file ]
121    _script_args += [
122      "--touch",
123      rebase_path(_stamp_file, root_build_dir),
124    ]
125  }
126
127  # Capture output or not (defaults to true).
128  if (!defined(invoker.capture_output) || invoker.capture_output) {
129    _script_args += [ "--capture-output" ]
130  }
131
132  if (defined(invoker.module)) {
133    _script_args += [
134      "--module",
135      invoker.module,
136    ]
137
138    # Pip installs should only ever need to occur in the Pigweed
139    # environment. For these actions do not use the build_dir virtualenv.
140    if (invoker.module == "pip") {
141      _use_build_dir_virtualenv = false
142    }
143  }
144
145  # Override to force using or not using the venv.
146  if (defined(invoker._pw_internal_run_in_venv)) {
147    _use_build_dir_virtualenv = invoker._pw_internal_run_in_venv
148  }
149
150  if (defined(invoker.working_directory)) {
151    _script_args += [
152      "--working-directory",
153      invoker.working_directory,
154    ]
155  }
156
157  if (defined(invoker.command_launcher)) {
158    _script_args += [
159      "--command-launcher",
160      invoker.command_launcher,
161    ]
162  }
163
164  if (defined(invoker._pw_action_type)) {
165    _action_type = invoker._pw_action_type
166  } else {
167    _action_type = "action"
168  }
169
170  if (defined(invoker.deps)) {
171    _deps = invoker.deps
172  } else {
173    _deps = []
174  }
175
176  _py_metadata_deps = []
177
178  if (defined(invoker.python_deps)) {
179    foreach(dep, invoker.python_deps) {
180      _deps += [ get_label_info(dep, "label_no_toolchain") + ".install(" +
181                 get_label_info(dep, "toolchain") + ")" ]
182
183      # Ensure each python_dep is added to the PYTHONPATH by depinding on the
184      # ._package_metadata subtarget.
185      _py_metadata_deps += [ get_label_info(dep, "label_no_toolchain") +
186                             "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
187    }
188
189    # Add the base target as a dep so the action reruns when any source files
190    # change, even if the package does not have to be reinstalled.
191    _deps += invoker.python_deps
192    _deps += _py_metadata_deps
193  }
194
195  # Check for additional PYTHONPATH dependencies.
196  _extra_python_metadata_deps = []
197  if (defined(invoker.python_metadata_deps)) {
198    foreach(dep, invoker.python_metadata_deps) {
199      _extra_python_metadata_deps +=
200          [ get_label_info(dep, "label_no_toolchain") +
201            "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
202    }
203  }
204
205  _metadata_path_list_file =
206      "${target_gen_dir}/${target_name}_metadata_path_list.txt"
207
208  # GN metadata only dependencies used for setting PYTHONPATH.
209  _metadata_deps = _py_metadata_deps + _extra_python_metadata_deps
210
211  # Build a list of relative paths containing all the python
212  # package_metadata.json files we depend on.
213  _metadata_path_list_target = "${target_name}._metadata_path_list.txt"
214  generated_file(_metadata_path_list_target) {
215    data_keys = [ "pw_python_package_metadata_json" ]
216    rebase = root_build_dir
217    deps = _metadata_deps
218    outputs = [ _metadata_path_list_file ]
219  }
220  _deps += [ ":${_metadata_path_list_target}" ]
221
222  # Set venv options if needed.
223  if (_use_build_dir_virtualenv) {
224    _venv_target_label = pw_build_PYTHON_BUILD_VENV
225    if (defined(invoker.venv)) {
226      _venv_target_label = invoker.venv
227    }
228    _venv_target_label =
229        get_label_info(_venv_target_label, "label_no_toolchain") +
230        "($pw_build_PYTHON_TOOLCHAIN)"
231
232    _venv_json =
233        get_label_info(_venv_target_label, "target_gen_dir") + "/" +
234        get_label_info(_venv_target_label, "name") + "/venv_metadata.json"
235    _script_args += [
236      "--python-virtualenv-config",
237      rebase_path(_venv_json, root_build_dir),
238    ]
239  }
240  _script_args += [
241    "--python-dep-list-files",
242    rebase_path(_metadata_path_list_file, root_build_dir),
243  ]
244
245  # "--" indicates the end of arguments to the runner script.
246  # Everything beyond this point is interpreted as the command and arguments
247  # of the Python script to run.
248  _script_args += [ "--" ]
249
250  if (defined(invoker.script)) {
251    _script_args += [ rebase_path(invoker.script, root_build_dir) ]
252  }
253
254  _forward_python_metadata_deps = false
255  if (defined(invoker._forward_python_metadata_deps)) {
256    _forward_python_metadata_deps = true
257  }
258  if (_forward_python_metadata_deps) {
259    _script_args += [
260      "--python-dep-list-files",
261      rebase_path(_metadata_path_list_file, root_build_dir),
262    ]
263  }
264
265  if (defined(invoker.args)) {
266    _script_args += invoker.args
267  }
268
269  # Assume third party PyPI deps should be available in the build_dir virtualenv.
270  _install_venv_3p_deps = true
271  if (!_use_build_dir_virtualenv ||
272      (defined(invoker._skip_installing_external_python_deps) &&
273       invoker._skip_installing_external_python_deps)) {
274    _install_venv_3p_deps = false
275  }
276
277  # Check that script or module is a present and not a no-op.
278  _run_script_or_module = false
279  if (defined(invoker.script) || defined(invoker.module)) {
280    _run_script_or_module = true
281  }
282
283  target(_action_type, target_name) {
284    _ignore_vars = [
285      "script",
286      "args",
287      "deps",
288      "inputs",
289      "outputs",
290    ]
291    forward_variables_from(invoker, "*", _ignore_vars)
292
293    script = "$dir_pw_build/py/pw_build/python_runner.py"
294    args = _script_args
295    inputs = _inputs
296    outputs = _outputs
297    deps = _deps
298
299    if (_install_venv_3p_deps && _run_script_or_module) {
300      deps += [ get_label_info(_venv_target_label, "label_no_toolchain") +
301                "._install_3p_deps($pw_build_PYTHON_TOOLCHAIN)" ]
302    }
303  }
304}
305
306# Runs pw_python_action once per file over a set of sources.
307#
308# This template brings pw_python_action's features to action_foreach. Usage is
309# the same as pw_python_action, except that sources must be provided and source
310# expansion (e.g. "{{source}}") may be used in args and outputs.
311#
312# See the pw_python_action and action_foreach documentation for full details.
313template("pw_python_action_foreach") {
314  assert(defined(invoker.sources) && invoker.sources != [],
315         "pw_python_action_foreach requires a list of one or more sources")
316
317  pw_python_action(target_name) {
318    if (defined(invoker.stamp) && invoker.stamp != false) {
319      if (invoker.stamp == true) {
320        # Use source file names in the generated stamp file path so they are
321        # unique for each source.
322        stamp = "$target_gen_dir/{{source_file_part}}.pw_pystamp"
323      } else {
324        stamp = invoker.stamp
325      }
326    } else {
327      stamp = false
328    }
329
330    forward_variables_from(invoker, "*", [ "stamp" ])
331
332    _pw_action_type = "action_foreach"
333  }
334}
335