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