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