1 # Copyright 2020 The Pigweed Authors
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
7 # https://www.apache.org/licenses/LICENSE-2.0
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
15 import("//build_overrides/pigweed.gni")
17 import("$dir_pw_build/input_group.gni")
18 import("$dir_pw_build/python_action.gni")
20 # Python packages provide the following targets as $target_name.$subtarget.
21 _python_subtargets = [
30 # Defines a Python package. GN Python packages contain several GN targets:
32 # - $name - Provides the Python files in the build, but does not take any
33 # actions. All subtargets depend on this target.
34 # - $name.lint - Runs static analyis tools on the Python code. This is a group
36 # - $name.lint.mypy - Runs mypy (if enabled).
37 # - $name.lint.pylint - Runs pylint (if enabled).
38 # - $name.tests - Runs all tests for this package.
39 # - $name.install - Installs the package in a venv.
40 # - $name.wheel - Builds a Python wheel for the package. (Not implemented.)
42 # All Python packages are instantiated with the default toolchain, regardless of
43 # the current toolchain.
45 # TODO(pwbug/239): Implement wheel building.
48 # setup: List of setup file paths (setup.py or pyproject.toml & setup.cfg),
49 # which must all be in the same directory.
50 # sources: Python sources files in the package.
51 # tests: Test files for this Python package.
52 # python_deps: Dependencies on other pw_python_packages in the GN build.
53 # python_test_deps: Test-only pw_python_package dependencies.
54 # other_deps: Dependencies on GN targets that are not pw_python_packages.
55 # inputs: Other files to track, such as package_data.
56 # lint: If true (default), applies mypy and pylint to the package. If false,
58 # pylintrc: Optional path to a pylintrc configuration file to use. If not
59 # provided, Pylint's default rcfile search is used. Pylint is executed
60 # from the package's setup directory, so pylintrc files in that directory
61 # will take precedence over others.
62 # mypy_ini: Optional path to a mypy configuration file to use. If not
63 # provided, mypy's default configuration file search is used. mypy is
64 # executed from the package's setup directory, so mypy.ini files in that
65 # directory will take precedence over others.
66 template("pw_python_package") {
67 if (defined(invoker.sources)) {
68 _all_py_files = invoker.sources
73 if (defined(invoker.tests)) {
74 _test_sources = invoker.tests
79 _all_py_files += _test_sources
81 # pw_python_script uses pw_python_package, but with a limited set of features.
82 # _pw_standalone signals that this target is actually a pw_python_script.
83 _is_package = !(defined(invoker._pw_standalone) && invoker._pw_standalone)
85 # Some build targets generate Python packages, setting _pw_generated to
87 _is_generated_package =
88 defined(invoker._pw_generated) && invoker._pw_generated
90 # Argument: invoker.lint = [true | false]; default = true.
91 # Default to false for generated packages, but allow overrides.
92 if (defined(invoker.lint)) {
93 _should_lint = invoker.lint
95 _should_lint = !_is_generated_package
99 assert(defined(invoker.setup) && invoker.setup != [],
100 "pw_python_package requires 'setup' to point to a setup.py file " +
101 "or pyproject.toml and setup.cfg files")
103 if (!_is_generated_package) {
104 _all_py_files += invoker.setup
107 # Get the directories of the setup files. All files must be in the same dir.
108 _setup_dirs = get_path_info(invoker.setup, "dir")
109 _setup_dir = _setup_dirs[0]
111 foreach(dir, _setup_dirs) {
112 assert(dir == _setup_dir,
113 "All files in 'setup' must be in the same directory")
116 # If sources are provided, make sure there is an __init__.py file.
117 if (!_is_generated_package && defined(invoker.sources) &&
118 invoker.sources != []) {
119 assert(filter_include(invoker.sources, [ "*\b__init__.py" ]) != [],
120 "Python packages must have at least one __init__.py file")
125 if (defined(invoker.python_deps)) {
126 foreach(dep, invoker.python_deps) {
127 # Use the fully qualified name so the subtarget can be appended as needed.
128 _python_deps += [ get_label_info(dep, "label_no_toolchain") ]
132 # All dependencies needed for the package and its tests.
133 _python_test_deps = _python_deps
134 if (defined(invoker.python_test_deps)) {
135 foreach(test_dep, invoker.python_test_deps) {
136 _python_test_deps += [ get_label_info(test_dep, "label_no_toolchain") ]
140 if (_test_sources == []) {
141 assert(!defined(invoker.python_test_deps),
142 "python_test_deps was provided, but there are no tests in " +
143 get_label_info(":$target_name", "label_no_toolchain"))
144 not_needed(_python_test_deps)
147 _internal_target = "$target_name._internal"
149 # Create groups with the public target names ($target_name, $target_name.lint,
150 # $target_name.install, etc.). These are actually wrappers around internal
151 # Python actions instantiated with the default toolchain. This ensures there
152 # is only a single copy of each Python action in the build.
154 # The $target_name.tests group is created separately below.
155 foreach(subtarget, _python_subtargets - [ "tests" ]) {
156 group("$target_name.$subtarget") {
157 deps = [ ":$_internal_target.$subtarget($default_toolchain)" ]
161 group("$target_name") {
162 deps = [ ":$_internal_target($default_toolchain)" ]
165 # Declare the main Python package group. This represents the Python files, but
166 # does not take any actions. GN targets can depend on the package name to run
167 # when any files in the package change.
168 pw_input_group("$_internal_target") {
169 inputs = _all_py_files
170 if (defined(invoker.inputs)) {
171 inputs += invoker.inputs
176 if (defined(invoker.other_deps)) {
177 deps += invoker.other_deps
182 # Install this Python package and its dependencies in the current Python
184 pw_python_action("$_internal_target.install") {
188 # Don't install generated packages with --editable, since the build
189 # directory is ephemeral.
190 if (!_is_generated_package) {
191 args += [ "--editable" ]
193 args += [ rebase_path(_setup_dir) ]
197 # Parallel pip installations don't work, so serialize pip invocations.
198 pool = "$dir_pw_build:pip_pool"
200 deps = [ ":$_internal_target" ]
201 foreach(dep, _python_deps) {
202 deps += [ "$dep.install" ]
206 # TODO(pwbug/239): Add support for building groups of wheels. The code below
207 # is incomplete and untested.
208 pw_python_action("$_internal_target.wheel") {
209 script = "$dir_pw_build/py/pw_build/python_wheels.py"
213 rebase_path(target_out_dir),
215 args += rebase_path(_all_py_files)
217 deps = [ ":$_internal_target.install" ]
221 # If this is not a package, install or build wheels for its deps only.
222 group("$_internal_target.install") {
224 foreach(dep, _python_deps) {
225 deps += [ "$dep.install" ]
228 group("$_internal_target.wheel") {
230 foreach(dep, _python_deps) {
231 deps += [ "$dep.wheel" ]
236 # Define the static analysis targets for this package.
237 group("$_internal_target.lint") {
239 ":$_internal_target.lint.mypy",
240 ":$_internal_target.lint.pylint",
244 if (_should_lint || _test_sources != []) {
245 # Packages that must be installed to use the package or run its tests.
246 _test_install_deps = [ ":$_internal_target.install" ]
247 foreach(dep, _python_test_deps + [ "$dir_pw_build:python_lint" ]) {
248 _test_install_deps += [ "$dep.install" ]
252 # For packages that are not generated, create targets to run mypy and pylint.
253 # Linting is not performed on generated packages.
255 # Run lint tools from the setup or target directory so that the tools detect
256 # config files (e.g. pylintrc or mypy.ini) in that directory. Config files
257 # may be explicitly specified with the pylintrc or mypy_ini arguments.
258 if (defined(_setup_dir)) {
259 _lint_directory = rebase_path(_setup_dir)
261 _lint_directory = rebase_path(".")
264 pw_python_action("$_internal_target.lint.mypy") {
268 "--show-error-codes",
271 if (defined(invoker.mypy_ini)) {
272 args += [ "--config-file=" + rebase_path(invoker.mypy_ini) ]
273 inputs = [ invoker.mypy_ini ]
276 args += rebase_path(_all_py_files)
278 # Use this environment variable to force mypy to colorize output.
279 # See https://github.com/python/mypy/issues/7771
280 environment = [ "MYPY_FORCE_COLOR=1" ]
282 directory = _lint_directory
285 deps = _test_install_deps
287 foreach(dep, _python_test_deps) {
288 deps += [ "$dep.lint.mypy" ]
292 # Create a target to run pylint on each of the Python files in this
293 # package and its dependencies.
294 pw_python_action_foreach("$_internal_target.lint.pylint") {
297 rebase_path(".") + "/{{source_target_relative}}",
299 "--output-format=colorized",
302 if (defined(invoker.pylintrc)) {
303 args += [ "--rcfile=" + rebase_path(invoker.pylintrc) ]
304 inputs = [ invoker.pylintrc ]
307 if (host_os == "win") {
308 # Allow CRLF on Windows, in case Git is set to switch line endings.
309 args += [ "--disable=unexpected-line-ending-format" ]
312 sources = _all_py_files
314 directory = _lint_directory
315 stamp = "$target_gen_dir/{{source_target_relative}}.pylint.passed"
317 deps = _test_install_deps
319 foreach(dep, _python_test_deps) {
320 deps += [ "$dep.lint.pylint" ]
324 pw_input_group("$_internal_target.lint.mypy") {
325 if (defined(invoker.pylintrc)) {
326 inputs = [ invoker.pylintrc ]
329 pw_input_group("$_internal_target.lint.pylint") {
330 if (defined(invoker.mypy_ini)) {
331 inputs = [ invoker.mypy_ini ]
336 # Create a target for each test file.
339 foreach(test, _test_sources) {
340 _test_name = string_replace(test, "/", "_")
341 _internal_test_target = "$_internal_target.tests.$_test_name"
343 pw_python_action(_internal_test_target) {
347 deps = _test_install_deps
349 foreach(dep, _python_test_deps) {
350 deps += [ "$dep.tests" ]
354 # Create a public version of each test target, so tests can be executed as
355 # //path/to:package.tests.foo.py.
356 group("$target_name.tests.$_test_name") {
357 deps = [ ":$_internal_test_target" ]
360 _test_targets += [ ":$target_name.tests.$_test_name" ]
363 group("$target_name.tests") {
368 # Declares a group of Python packages or other Python groups. pw_python_groups
369 # expose the same set of subtargets as pw_python_package (e.g.
370 # "$group_name.lint" and "$group_name.tests"), but these apply to all packages
371 # in deps and their dependencies.
372 template("pw_python_group") {
373 if (defined(invoker.python_deps)) {
374 _python_deps = invoker.python_deps
383 foreach(subtarget, _python_subtargets) {
384 group("$target_name.$subtarget") {
386 foreach(dep, _python_deps) {
387 # Split out the toolchain to support deps with a toolchain specified.
388 _target = get_label_info(dep, "label_no_toolchain")
389 _toolchain = get_label_info(dep, "toolchain")
390 deps += [ "$_target.$subtarget($_toolchain)" ]
396 # Declares Python scripts or tests that are not part of a Python package.
397 # Similar to pw_python_package, but only supports a subset of its features.
399 # pw_python_script accepts the same arguments as pw_python_package, except
400 # `setup` cannot be provided.
402 # pw_python_script provides the same subtargets as pw_python_package, but
403 # $target_name.install and $target_name.wheel only affect the python_deps of
404 # this GN target, not the target itself.
405 template("pw_python_script") {
406 _supported_variables = [
415 pw_python_package(target_name) {
416 _pw_standalone = true
417 forward_variables_from(invoker, _supported_variables)
421 # Represents a list of Python requirements, as in a requirements.txt.
424 # files: One or more requirements.txt files.
425 # requirements: A list of requirements.txt-style requirements.
426 template("pw_python_requirements") {
427 assert(defined(invoker.files) || defined(invoker.requirements),
428 "pw_python_requirements requires a list of requirements.txt files " +
429 "in the 'files' arg or requirements in 'requirements'")
431 _requirements_files = []
433 if (defined(invoker.files)) {
434 _requirements_files += invoker.files
437 if (defined(invoker.requirements)) {
438 _requirements_file = "$target_gen_dir/$target_name.requirements.txt"
439 write_file(_requirements_file, invoker.requirements)
440 _requirements_files += [ _requirements_file ]
443 # The default target represents the requirements themselves.
444 pw_input_group(target_name) {
445 inputs = _requirements_files
448 # Use the same subtargets as pw_python_package so these targets can be listed
449 # as python_deps of pw_python_packages.
450 pw_python_action("$target_name.install") {
451 inputs = _requirements_files
456 foreach(_requirements_file, inputs) {
459 rebase_path(_requirements_file),
463 pool = "$dir_pw_build:pip_pool"
467 # Create stubs for the unused subtargets so that pw_python_requirements can be
468 # used as python_deps.
469 foreach(subtarget, _python_subtargets - [ "install" ]) {
470 group("$target_name.$subtarget") {