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