1# Copyright 2021 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/input_group.gni") 18import("$dir_pw_build/mirror_tree.gni") 19import("$dir_pw_build/python_action.gni") 20 21# Python packages provide the following targets as $target_name.$subtarget. 22pw_python_package_subtargets = [ 23 "tests", 24 "lint", 25 "lint.mypy", 26 "lint.pylint", 27 "install", 28 "wheel", 29 30 # Internal targets that directly depend on one another. 31 "_run_pip_install", 32 "_build_wheel", 33] 34 35# Create aliases for subsargets when the target name matches the directory name. 36# This allows //foo:foo.tests to be accessed as //foo:tests, for example. 37template("_pw_create_aliases_if_name_matches_directory") { 38 not_needed([ "invoker" ]) 39 40 if (get_label_info(":$target_name", "name") == 41 get_path_info(get_label_info(":$target_name", "dir"), "name")) { 42 foreach(subtarget, pw_python_package_subtargets) { 43 group(subtarget) { 44 public_deps = [ ":${invoker.target_name}.$subtarget" ] 45 } 46 } 47 } 48} 49 50# Internal template that runs Mypy. 51template("_pw_python_static_analysis_mypy") { 52 pw_python_action(target_name) { 53 module = "mypy" 54 args = [ 55 "--pretty", 56 "--show-error-codes", 57 ] 58 59 if (defined(invoker.mypy_ini)) { 60 args += [ "--config-file=" + rebase_path(invoker.mypy_ini) ] 61 inputs = [ invoker.mypy_ini ] 62 } 63 64 args += rebase_path(invoker.sources) 65 66 # Use this environment variable to force mypy to colorize output. 67 # See https://github.com/python/mypy/issues/7771 68 environment = [ "MYPY_FORCE_COLOR=1" ] 69 70 directory = invoker.directory 71 stamp = true 72 73 deps = invoker.deps 74 75 foreach(dep, invoker.python_deps) { 76 deps += [ string_replace(dep, "(", ".lint.mypy(") ] 77 } 78 } 79} 80 81# Internal template that runs Pylint. 82template("_pw_python_static_analysis_pylint") { 83 # Create a target to run pylint on each of the Python files in this 84 # package and its dependencies. 85 pw_python_action_foreach(target_name) { 86 module = "pylint" 87 args = [ 88 rebase_path(".") + "/{{source_target_relative}}", 89 "--jobs=1", 90 "--output-format=colorized", 91 ] 92 93 if (defined(invoker.pylintrc)) { 94 args += [ "--rcfile=" + rebase_path(invoker.pylintrc) ] 95 inputs = [ invoker.pylintrc ] 96 } 97 98 if (host_os == "win") { 99 # Allow CRLF on Windows, in case Git is set to switch line endings. 100 args += [ "--disable=unexpected-line-ending-format" ] 101 } 102 103 sources = invoker.sources 104 directory = invoker.directory 105 106 stamp = "$target_gen_dir/{{source_target_relative}}.pylint.passed" 107 108 public_deps = invoker.deps 109 110 foreach(dep, invoker.python_deps) { 111 public_deps += [ string_replace(dep, "(", ".lint.pylint(") ] 112 } 113 } 114} 115 116# Defines a Python package. GN Python packages contain several GN targets: 117# 118# - $name - Provides the Python files in the build, but does not take any 119# actions. All subtargets depend on this target. 120# - $name.lint - Runs static analyis tools on the Python code. This is a group 121# of two subtargets: 122# - $name.lint.mypy - Runs mypy (if enabled). 123# - $name.lint.pylint - Runs pylint (if enabled). 124# - $name.tests - Runs all tests for this package. 125# - $name.install - Installs the package in a venv. 126# - $name.wheel - Builds a Python wheel for the package. 127# 128# All Python packages are instantiated with the default toolchain, regardless of 129# the current toolchain. 130# 131# Args: 132# setup: List of setup file paths (setup.py or pyproject.toml & setup.cfg), 133# which must all be in the same directory. 134# generate_setup: As an alternative to 'setup', generate setup files with the 135# keywords in this scope. 'name' is required. 136# sources: Python sources files in the package. 137# tests: Test files for this Python package. 138# python_deps: Dependencies on other pw_python_packages in the GN build. 139# python_test_deps: Test-only pw_python_package dependencies. 140# other_deps: Dependencies on GN targets that are not pw_python_packages. 141# inputs: Other files to track, such as package_data. 142# proto_library: A pw_proto_library target to embed in this Python package. 143# generate_setup is required in place of setup if proto_library is used. 144# static_analysis: List of static analysis tools to run; "*" (default) runs 145# all tools. The supported tools are "mypy" and "pylint". 146# pylintrc: Optional path to a pylintrc configuration file to use. If not 147# provided, Pylint's default rcfile search is used. Pylint is executed 148# from the package's setup directory, so pylintrc files in that directory 149# will take precedence over others. 150# mypy_ini: Optional path to a mypy configuration file to use. If not 151# provided, mypy's default configuration file search is used. mypy is 152# executed from the package's setup directory, so mypy.ini files in that 153# directory will take precedence over others. 154# 155template("pw_python_package") { 156 # The Python targets are always instantiated in the default toolchain. Use 157 # fully qualified labels so that the toolchain is not lost. 158 _other_deps = [] 159 if (defined(invoker.other_deps)) { 160 foreach(dep, invoker.other_deps) { 161 _other_deps += [ get_label_info(dep, "label_with_toolchain") ] 162 } 163 } 164 165 _python_deps = [] 166 if (defined(invoker.python_deps)) { 167 foreach(dep, invoker.python_deps) { 168 _python_deps += [ get_label_info(dep, "label_with_toolchain") ] 169 } 170 } 171 172 # pw_python_script uses pw_python_package, but with a limited set of features. 173 # _pw_standalone signals that this target is actually a pw_python_script. 174 _is_package = !(defined(invoker._pw_standalone) && invoker._pw_standalone) 175 176 _generate_package = false 177 178 # Check the generate_setup and import_protos args to determine if this package 179 # is generated. 180 if (_is_package) { 181 assert(defined(invoker.generate_setup) != defined(invoker.setup), 182 "Either 'setup' or 'generate_setup' (but not both) must provided") 183 184 if (defined(invoker.proto_library)) { 185 assert(invoker.proto_library != "", "'proto_library' cannot be empty") 186 assert(defined(invoker.generate_setup), 187 "Python packages that import protos with 'proto_library' must " + 188 "use 'generate_setup' instead of 'setup'") 189 190 _import_protos = [ invoker.proto_library ] 191 } else if (defined(invoker.generate_setup)) { 192 _import_protos = [] 193 } 194 195 if (defined(invoker.generate_setup)) { 196 _generate_package = true 197 _setup_dir = "$target_gen_dir/$target_name.generated_python_package" 198 199 if (defined(invoker.strip_prefix)) { 200 _source_root = invoker.strip_prefix 201 } else { 202 _source_root = "." 203 } 204 } else { 205 # Non-generated packages with sources provided need an __init__.py. 206 assert(!defined(invoker.sources) || invoker.sources == [] || 207 filter_include(invoker.sources, [ "*\b__init__.py" ]) != [], 208 "Python packages must have at least one __init__.py file") 209 210 # Get the directories of the setup files. All must be in the same dir. 211 _setup_dirs = get_path_info(invoker.setup, "dir") 212 _setup_dir = _setup_dirs[0] 213 214 foreach(dir, _setup_dirs) { 215 assert(dir == _setup_dir, 216 "All files in 'setup' must be in the same directory") 217 } 218 219 assert(!defined(invoker.strip_prefix), 220 "'strip_prefix' may only be given if 'generate_setup' is provided") 221 } 222 } 223 224 # Process arguments defaults and set defaults. 225 226 _supported_static_analysis_tools = [ 227 "mypy", 228 "pylint", 229 ] 230 not_needed([ "_supported_static_analysis_tools" ]) 231 232 # Argument: static_analysis (list of tool names or "*"); default = "*" (all) 233 if (!defined(invoker.static_analysis) || invoker.static_analysis == "*") { 234 _static_analysis = _supported_static_analysis_tools 235 } else { 236 _static_analysis = invoker.static_analysis 237 } 238 239 # TODO(hepler): Remove support for the lint option. 240 if (defined(invoker.lint)) { 241 assert(!defined(invoker.static_analysis), 242 "'lint' is deprecated; use 'static_analysis' instead") 243 244 # Only allow 'lint = false', for backwards compatibility. 245 assert(invoker.lint == false, "'lint' is deprecated; use 'static_analysis'") 246 print("WARNING:", 247 "The 'lint' option for pw_python_package is deprecated.", 248 "Instead, use 'static_analysis = []' to disable linting.") 249 _static_analysis = [] 250 } 251 252 foreach(_tool, _static_analysis) { 253 assert(_supported_static_analysis_tools + [ _tool ] - [ _tool ] != 254 _supported_static_analysis_tools, 255 "'$_tool' is not a supported static analysis tool") 256 } 257 258 # Argument: sources (list) 259 _sources = [] 260 if (defined(invoker.sources)) { 261 if (_generate_package) { 262 foreach(source, rebase_path(invoker.sources, _source_root)) { 263 _sources += [ "$_setup_dir/$source" ] 264 } 265 } else { 266 _sources += invoker.sources 267 } 268 } 269 270 # Argument: tests (list) 271 _test_sources = [] 272 if (defined(invoker.tests)) { 273 if (_generate_package) { 274 foreach(source, rebase_path(invoker.tests, _source_root)) { 275 _test_sources += [ "$_setup_dir/$source" ] 276 } 277 } else { 278 _test_sources += invoker.tests 279 } 280 } 281 282 # Argument: setup (list) 283 _setup_sources = [] 284 if (defined(invoker.setup)) { 285 _setup_sources = invoker.setup 286 } else if (_generate_package) { 287 _setup_sources = [ "$_setup_dir/setup.py" ] 288 } 289 290 # Argument: python_test_deps (list) 291 _python_test_deps = _python_deps # include all deps in test deps 292 if (defined(invoker.python_test_deps)) { 293 foreach(dep, invoker.python_test_deps) { 294 _python_test_deps += [ get_label_info(dep, "label_with_toolchain") ] 295 } 296 } 297 298 if (_test_sources == []) { 299 assert(!defined(invoker.python_test_deps), 300 "python_test_deps was provided, but there are no tests in " + 301 get_label_info(":$target_name", "label_no_toolchain")) 302 not_needed([ "_python_test_deps" ]) 303 } 304 305 _all_py_files = _sources + _test_sources + _setup_sources 306 307 # The pw_python_package subtargets are only instantiated in the default 308 # toolchain. Other toolchains just refer to targets in the default toolchain. 309 if (current_toolchain == default_toolchain) { 310 # Declare the main Python package group. This represents the Python files, 311 # but does not take any actions. GN targets can depend on the package name 312 # to run when any files in the package change. 313 if (_generate_package) { 314 # If this package is generated, mirror the sources to the final directory. 315 pw_mirror_tree("$target_name._mirror_sources_to_out_dir") { 316 directory = _setup_dir 317 318 sources = [] 319 if (defined(invoker.sources)) { 320 sources += invoker.sources 321 } 322 if (defined(invoker.tests)) { 323 sources += invoker.tests 324 } 325 326 source_root = _source_root 327 public_deps = _python_deps + _other_deps 328 } 329 330 # Depend on the proto's _gen targets (from the default toolchain). 331 _gen_protos = [] 332 foreach(proto, _import_protos) { 333 _gen_protos += 334 [ get_label_info(proto, "label_no_toolchain") + ".python._gen" ] 335 } 336 337 generated_file("$target_name._protos") { 338 deps = _gen_protos 339 data_keys = [ "protoc_outputs" ] 340 outputs = [ "$_setup_dir/protos.txt" ] 341 } 342 343 _protos_file = get_target_outputs(":${invoker.target_name}._protos") 344 345 generated_file("$target_name._protos_root") { 346 deps = _gen_protos 347 data_keys = [ "root" ] 348 outputs = [ "$_setup_dir/proto_root.txt" ] 349 } 350 351 _root_file = get_target_outputs(":${invoker.target_name}._protos_root") 352 353 # Get generated_setup scope and write it to disk ask JSON. 354 _gen_setup = invoker.generate_setup 355 assert(defined(_gen_setup.name), "'name' is required in generate_package") 356 assert(!defined(_gen_setup.packages) && !defined(_gen_setup.package_data), 357 "'packages' and 'package_data' may not be provided " + 358 "in 'generate_package'") 359 write_file("$_setup_dir/setup.json", _gen_setup, "json") 360 361 # Generate the setup.py, py.typed, and __init__.py files as needed. 362 action(target_name) { 363 script = "$dir_pw_build/py/pw_build/generate_python_package.py" 364 args = [ 365 "--label", 366 get_label_info(":$target_name", "label_no_toolchain"), 367 "--root", 368 rebase_path(_setup_dir), 369 "--setup-json", 370 rebase_path("$_setup_dir/setup.json"), 371 "--file-list", 372 rebase_path(_protos_file[0]), 373 "--file-list-root", 374 rebase_path(_root_file[0]), 375 ] + rebase_path(_sources) 376 377 if (defined(invoker._pw_module_as_package) && 378 invoker._pw_module_as_package) { 379 args += [ "--module-as-package" ] 380 } 381 382 inputs = [ "$_setup_dir/setup.json" ] 383 384 public_deps = [ 385 ":$target_name._mirror_sources_to_out_dir", 386 ":$target_name._protos", 387 ":$target_name._protos_root", 388 ] 389 390 foreach(proto, _import_protos) { 391 _tgt = get_label_info(proto, "label_no_toolchain") 392 _path = get_label_info("$_tgt($default_toolchain)", "target_gen_dir") 393 _name = get_label_info(_tgt, "name") 394 395 args += [ 396 "--proto-library=$_tgt", 397 "--proto-library-file", 398 rebase_path("$_path/$_name.proto_library/python_package.txt"), 399 ] 400 401 public_deps += [ "$_tgt.python._gen($default_toolchain)" ] 402 } 403 404 outputs = _setup_sources 405 } 406 } else { 407 # If the package is not generated, use an input group for the sources. 408 pw_input_group(target_name) { 409 inputs = _all_py_files 410 if (defined(invoker.inputs)) { 411 inputs += invoker.inputs 412 } 413 414 deps = _python_deps + _other_deps 415 } 416 } 417 418 if (_is_package) { 419 # Install this Python package and its dependencies in the current Python 420 # environment using pip. 421 pw_python_action("$target_name._run_pip_install") { 422 module = "pip" 423 public_deps = [] 424 425 args = [ "install" ] 426 427 # For generated packages, reinstall when any files change. For regular 428 # packages, only reinstall when setup.py changes. 429 if (_generate_package) { 430 public_deps += [ ":${invoker.target_name}" ] 431 } else { 432 inputs = invoker.setup 433 434 # Install with --editable since the complete package is in source. 435 args += [ "--editable" ] 436 } 437 438 args += [ rebase_path(_setup_dir) ] 439 440 stamp = true 441 442 # Parallel pip installations don't work, so serialize pip invocations. 443 pool = "$dir_pw_build:pip_pool" 444 445 foreach(dep, _python_deps) { 446 # We need to add a suffix to the target name, but the label is 447 # formatted as "//path/to:target(toolchain)", so we can't just append 448 # ".subtarget". Instead, we replace the opening parenthesis of the 449 # toolchain with ".suffix(". 450 public_deps += [ string_replace(dep, "(", "._run_pip_install(") ] 451 } 452 } 453 454 # Builds a Python wheel for this package. Records the output directory 455 # in the pw_python_package_wheels metadata key. 456 pw_python_action("$target_name._build_wheel") { 457 metadata = { 458 pw_python_package_wheels = [ "$target_out_dir/$target_name" ] 459 } 460 461 module = "build" 462 463 args = [ 464 rebase_path(_setup_dir), 465 "--wheel", 466 "--no-isolation", 467 "--outdir", 468 ] + rebase_path(metadata.pw_python_package_wheels) 469 470 deps = [ ":${invoker.target_name}" ] 471 foreach(dep, _python_deps) { 472 deps += [ string_replace(dep, "(", ".wheel(") ] 473 } 474 475 stamp = true 476 } 477 } else { 478 # Stubs for non-package targets. 479 group("$target_name._run_pip_install") { 480 } 481 group("$target_name._build_wheel") { 482 } 483 } 484 485 # Create the .install and .wheel targets. To limit unnecessary pip 486 # executions, non-generated packages are only reinstalled when their 487 # setup.py changes. However, targets that depend on the .install subtarget 488 # re-run whenever any source files change. 489 # 490 # These targets just represent the source files if this isn't a package. 491 group("$target_name.install") { 492 public_deps = [ ":${invoker.target_name}" ] 493 494 if (_is_package) { 495 public_deps += [ ":${invoker.target_name}._run_pip_install" ] 496 } 497 498 foreach(dep, _python_deps) { 499 public_deps += [ string_replace(dep, "(", ".install(") ] 500 } 501 } 502 503 group("$target_name.wheel") { 504 public_deps = [ ":${invoker.target_name}.install" ] 505 506 if (_is_package) { 507 public_deps += [ ":${invoker.target_name}._build_wheel" ] 508 } 509 510 foreach(dep, _python_deps) { 511 public_deps += [ string_replace(dep, "(", ".wheel(") ] 512 } 513 } 514 515 # Define the static analysis targets for this package. 516 group("$target_name.lint") { 517 deps = [] 518 foreach(_tool, _supported_static_analysis_tools) { 519 deps += [ ":${invoker.target_name}.lint.$_tool" ] 520 } 521 } 522 523 if (_static_analysis != [] || _test_sources != []) { 524 # All packages to install for either general use or test running. 525 _test_install_deps = [ ":$target_name.install" ] 526 foreach(dep, _python_test_deps + [ "$dir_pw_build:python_lint" ]) { 527 _test_install_deps += [ string_replace(dep, "(", ".install(") ] 528 } 529 } 530 531 # For packages that are not generated, create targets to run mypy and pylint. 532 foreach(_tool, _static_analysis) { 533 # Run lint tools from the setup or target directory so that the tools detect 534 # config files (e.g. pylintrc or mypy.ini) in that directory. Config files 535 # may be explicitly specified with the pylintrc or mypy_ini arguments. 536 target("_pw_python_static_analysis_$_tool", "$target_name.lint.$_tool") { 537 sources = _all_py_files 538 deps = _test_install_deps 539 python_deps = _python_deps 540 541 if (defined(_setup_dir)) { 542 directory = rebase_path(_setup_dir) 543 } else { 544 directory = rebase_path(".") 545 } 546 547 _optional_variables = [ 548 "mypy_ini", 549 "pylintrc", 550 ] 551 forward_variables_from(invoker, _optional_variables) 552 not_needed(_optional_variables) 553 } 554 } 555 556 foreach(_unused_tool, _supported_static_analysis_tools - _static_analysis) { 557 pw_input_group("$target_name.lint.$_unused_tool") { 558 inputs = [] 559 if (defined(invoker.pylintrc)) { 560 inputs += [ invoker.pylintrc ] 561 } 562 if (defined(invoker.mypy_ini)) { 563 inputs += [ invoker.mypy_ini ] 564 } 565 } 566 567 # Generated packages with linting disabled never need the whole file list. 568 not_needed([ "_all_py_files" ]) 569 } 570 } else { 571 # Create groups with the public target names ($target_name, $target_name.lint, 572 # $target_name.install, etc.). These are actually wrappers around internal 573 # Python actions instantiated with the default toolchain. This ensures there 574 # is only a single copy of each Python action in the build. 575 # 576 # The $target_name.tests group is created separately below. 577 group("$target_name") { 578 deps = [ ":$target_name($default_toolchain)" ] 579 } 580 581 foreach(subtarget, pw_python_package_subtargets - [ "tests" ]) { 582 group("$target_name.$subtarget") { 583 deps = [ ":${invoker.target_name}.$subtarget($default_toolchain)" ] 584 } 585 } 586 587 # Everything Python-related is only instantiated in the default toolchain. 588 # Silence not-needed warnings except for in the default toolchain. 589 not_needed("*") 590 not_needed(invoker, "*") 591 } 592 593 # Create a target for each test file. 594 _test_targets = [] 595 596 foreach(test, _test_sources) { 597 if (_is_package) { 598 _name = rebase_path(test, _setup_dir) 599 } else { 600 _name = test 601 } 602 603 _test_target = "$target_name.tests." + string_replace(_name, "/", "_") 604 605 if (current_toolchain == default_toolchain) { 606 pw_python_action(_test_target) { 607 script = test 608 stamp = true 609 610 deps = _test_install_deps 611 612 foreach(dep, _python_test_deps) { 613 deps += [ string_replace(dep, "(", ".tests(") ] 614 } 615 } 616 } else { 617 # Create a public version of each test target, so tests can be executed as 618 # //path/to:package.tests.foo.py. 619 group(_test_target) { 620 deps = [ ":$_test_target($default_toolchain)" ] 621 } 622 } 623 624 _test_targets += [ ":$_test_target" ] 625 } 626 627 group("$target_name.tests") { 628 deps = _test_targets 629 } 630 631 _pw_create_aliases_if_name_matches_directory(target_name) { 632 } 633} 634 635# Declares a group of Python packages or other Python groups. pw_python_groups 636# expose the same set of subtargets as pw_python_package (e.g. 637# "$group_name.lint" and "$group_name.tests"), but these apply to all packages 638# in deps and their dependencies. 639template("pw_python_group") { 640 if (defined(invoker.python_deps)) { 641 _python_deps = invoker.python_deps 642 } else { 643 _python_deps = [] 644 } 645 646 group(target_name) { 647 deps = _python_deps 648 } 649 650 foreach(subtarget, pw_python_package_subtargets) { 651 group("$target_name.$subtarget") { 652 public_deps = [] 653 foreach(dep, _python_deps) { 654 # Split out the toolchain to support deps with a toolchain specified. 655 _target = get_label_info(dep, "label_no_toolchain") 656 _toolchain = get_label_info(dep, "toolchain") 657 public_deps += [ "$_target.$subtarget($_toolchain)" ] 658 } 659 } 660 } 661 662 _pw_create_aliases_if_name_matches_directory(target_name) { 663 } 664} 665 666# Declares Python scripts or tests that are not part of a Python package. 667# Similar to pw_python_package, but only supports a subset of its features. 668# 669# pw_python_script accepts the same arguments as pw_python_package, except 670# `setup` cannot be provided. 671# 672# pw_python_script provides the same subtargets as pw_python_package, but 673# $target_name.install and $target_name.wheel only affect the python_deps of 674# this GN target, not the target itself. 675template("pw_python_script") { 676 _supported_variables = [ 677 "sources", 678 "tests", 679 "python_deps", 680 "other_deps", 681 "inputs", 682 "pylintrc", 683 "mypy_ini", 684 "static_analysis", 685 ] 686 687 pw_python_package(target_name) { 688 _pw_standalone = true 689 forward_variables_from(invoker, _supported_variables) 690 } 691 692 _pw_create_aliases_if_name_matches_directory(target_name) { 693 } 694} 695 696# Represents a list of Python requirements, as in a requirements.txt. 697# 698# Args: 699# files: One or more requirements.txt files. 700# requirements: A list of requirements.txt-style requirements. 701template("pw_python_requirements") { 702 assert(defined(invoker.files) || defined(invoker.requirements), 703 "pw_python_requirements requires a list of requirements.txt files " + 704 "in the 'files' arg or requirements in 'requirements'") 705 706 _requirements_files = [] 707 708 if (defined(invoker.files)) { 709 _requirements_files += invoker.files 710 } 711 712 if (defined(invoker.requirements)) { 713 _requirements_file = "$target_gen_dir/$target_name.requirements.txt" 714 write_file(_requirements_file, invoker.requirements) 715 _requirements_files += [ _requirements_file ] 716 } 717 718 # The default target represents the requirements themselves. 719 pw_input_group(target_name) { 720 inputs = _requirements_files 721 } 722 723 # Use the same subtargets as pw_python_package so these targets can be listed 724 # as python_deps of pw_python_packages. 725 pw_python_action("$target_name.install") { 726 inputs = _requirements_files 727 728 module = "pip" 729 args = [ "install" ] 730 731 foreach(_requirements_file, inputs) { 732 args += [ 733 "--requirement", 734 rebase_path(_requirements_file), 735 ] 736 } 737 738 pool = "$dir_pw_build:pip_pool" 739 stamp = true 740 } 741 742 # Create stubs for the unused subtargets so that pw_python_requirements can be 743 # used as python_deps. 744 foreach(subtarget, pw_python_package_subtargets - [ "install" ]) { 745 group("$target_name.$subtarget") { 746 } 747 } 748 749 _pw_create_aliases_if_name_matches_directory(target_name) { 750 } 751} 752