Fix for x86_64 build fail
[platform/upstream/connectedhomeip.git] / third_party / pigweed / repo / pw_build / python.gni
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
15 import("//build_overrides/pigweed.gni")
16
17 import("$dir_pw_build/input_group.gni")
18 import("$dir_pw_build/python_action.gni")
19
20 # Python packages provide the following targets as $target_name.$subtarget.
21 _python_subtargets = [
22   "tests",
23   "lint",
24   "lint.mypy",
25   "lint.pylint",
26   "install",
27   "wheel",
28 ]
29
30 # Defines a Python package. GN Python packages contain several GN targets:
31 #
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
35 #     of two subtargets:
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.)
41 #
42 # All Python packages are instantiated with the default toolchain, regardless of
43 # the current toolchain.
44 #
45 # TODO(pwbug/239): Implement wheel building.
46 #
47 # Args:
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,
57 #       does not.
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
69   } else {
70     _all_py_files = []
71   }
72
73   if (defined(invoker.tests)) {
74     _test_sources = invoker.tests
75   } else {
76     _test_sources = []
77   }
78
79   _all_py_files += _test_sources
80
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)
84
85   # Some build targets generate Python packages, setting _pw_generated to
86   # indicate this.
87   _is_generated_package =
88       defined(invoker._pw_generated) && invoker._pw_generated
89
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
94   } else {
95     _should_lint = !_is_generated_package
96   }
97
98   if (_is_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")
102
103     if (!_is_generated_package) {
104       _all_py_files += invoker.setup
105     }
106
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]
110
111     foreach(dir, _setup_dirs) {
112       assert(dir == _setup_dir,
113              "All files in 'setup' must be in the same directory")
114     }
115
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")
121     }
122   }
123
124   _python_deps = []
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") ]
129     }
130   }
131
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") ]
137     }
138   }
139
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)
145   }
146
147   _internal_target = "$target_name._internal"
148
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.
153   #
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)" ]
158     }
159   }
160
161   group("$target_name") {
162     deps = [ ":$_internal_target($default_toolchain)" ]
163   }
164
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
172     }
173
174     deps = _python_deps
175
176     if (defined(invoker.other_deps)) {
177       deps += invoker.other_deps
178     }
179   }
180
181   if (_is_package) {
182     # Install this Python package and its dependencies in the current Python
183     # environment.
184     pw_python_action("$_internal_target.install") {
185       module = "pip"
186       args = [ "install" ]
187
188       # Don't install generated packages with --editable, since the build
189       # directory is ephemeral.
190       if (!_is_generated_package) {
191         args += [ "--editable" ]
192       }
193       args += [ rebase_path(_setup_dir) ]
194
195       stamp = true
196
197       # Parallel pip installations don't work, so serialize pip invocations.
198       pool = "$dir_pw_build:pip_pool"
199
200       deps = [ ":$_internal_target" ]
201       foreach(dep, _python_deps) {
202         deps += [ "$dep.install" ]
203       }
204     }
205
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"
210
211       args = [
212         "--out_dir",
213         rebase_path(target_out_dir),
214       ]
215       args += rebase_path(_all_py_files)
216
217       deps = [ ":$_internal_target.install" ]
218       stamp = true
219     }
220   } else {
221     # If this is not a package, install or build wheels for its deps only.
222     group("$_internal_target.install") {
223       deps = []
224       foreach(dep, _python_deps) {
225         deps += [ "$dep.install" ]
226       }
227     }
228     group("$_internal_target.wheel") {
229       deps = []
230       foreach(dep, _python_deps) {
231         deps += [ "$dep.wheel" ]
232       }
233     }
234   }
235
236   # Define the static analysis targets for this package.
237   group("$_internal_target.lint") {
238     deps = [
239       ":$_internal_target.lint.mypy",
240       ":$_internal_target.lint.pylint",
241     ]
242   }
243
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" ]
249     }
250   }
251
252   # For packages that are not generated, create targets to run mypy and pylint.
253   # Linting is not performed on generated packages.
254   if (_should_lint) {
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)
260     } else {
261       _lint_directory = rebase_path(".")
262     }
263
264     pw_python_action("$_internal_target.lint.mypy") {
265       module = "mypy"
266       args = [
267         "--pretty",
268         "--show-error-codes",
269       ]
270
271       if (defined(invoker.mypy_ini)) {
272         args += [ "--config-file=" + rebase_path(invoker.mypy_ini) ]
273         inputs = [ invoker.mypy_ini ]
274       }
275
276       args += rebase_path(_all_py_files)
277
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" ]
281
282       directory = _lint_directory
283       stamp = true
284
285       deps = _test_install_deps
286
287       foreach(dep, _python_test_deps) {
288         deps += [ "$dep.lint.mypy" ]
289       }
290     }
291
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") {
295       module = "pylint"
296       args = [
297         rebase_path(".") + "/{{source_target_relative}}",
298         "--jobs=1",
299         "--output-format=colorized",
300       ]
301
302       if (defined(invoker.pylintrc)) {
303         args += [ "--rcfile=" + rebase_path(invoker.pylintrc) ]
304         inputs = [ invoker.pylintrc ]
305       }
306
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" ]
310       }
311
312       sources = _all_py_files
313
314       directory = _lint_directory
315       stamp = "$target_gen_dir/{{source_target_relative}}.pylint.passed"
316
317       deps = _test_install_deps
318
319       foreach(dep, _python_test_deps) {
320         deps += [ "$dep.lint.pylint" ]
321       }
322     }
323   } else {
324     pw_input_group("$_internal_target.lint.mypy") {
325       if (defined(invoker.pylintrc)) {
326         inputs = [ invoker.pylintrc ]
327       }
328     }
329     pw_input_group("$_internal_target.lint.pylint") {
330       if (defined(invoker.mypy_ini)) {
331         inputs = [ invoker.mypy_ini ]
332       }
333     }
334   }
335
336   # Create a target for each test file.
337   _test_targets = []
338
339   foreach(test, _test_sources) {
340     _test_name = string_replace(test, "/", "_")
341     _internal_test_target = "$_internal_target.tests.$_test_name"
342
343     pw_python_action(_internal_test_target) {
344       script = test
345       stamp = true
346
347       deps = _test_install_deps
348
349       foreach(dep, _python_test_deps) {
350         deps += [ "$dep.tests" ]
351       }
352     }
353
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" ]
358     }
359
360     _test_targets += [ ":$target_name.tests.$_test_name" ]
361   }
362
363   group("$target_name.tests") {
364     deps = _test_targets
365   }
366 }
367
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
375   } else {
376     _python_deps = []
377   }
378
379   group(target_name) {
380     deps = _python_deps
381   }
382
383   foreach(subtarget, _python_subtargets) {
384     group("$target_name.$subtarget") {
385       deps = []
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)" ]
391       }
392     }
393   }
394 }
395
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.
398 #
399 # pw_python_script accepts the same arguments as pw_python_package, except
400 # `setup` cannot be provided.
401 #
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 = [
407     "sources",
408     "tests",
409     "python_deps",
410     "other_deps",
411     "inputs",
412     "pylintrc",
413   ]
414
415   pw_python_package(target_name) {
416     _pw_standalone = true
417     forward_variables_from(invoker, _supported_variables)
418   }
419 }
420
421 # Represents a list of Python requirements, as in a requirements.txt.
422 #
423 # Args:
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'")
430
431   _requirements_files = []
432
433   if (defined(invoker.files)) {
434     _requirements_files += invoker.files
435   }
436
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 ]
441   }
442
443   # The default target represents the requirements themselves.
444   pw_input_group(target_name) {
445     inputs = _requirements_files
446   }
447
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
452
453     module = "pip"
454     args = [ "install" ]
455
456     foreach(_requirements_file, inputs) {
457       args += [
458         "--requirement",
459         rebase_path(_requirements_file),
460       ]
461     }
462
463     pool = "$dir_pw_build:pip_pool"
464     stamp = true
465   }
466
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") {
471     }
472   }
473 }