xref: /aosp_15_r20/external/pigweed/docs/python_build.rst (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1.. _docs-python-build:
2
3=========================
4Pigweed's GN Python Build
5=========================
6
7.. seealso::
8   - :bdg-ref-primary-line:`module-pw_build-python` for detailed template usage.
9   - :bdg-ref-primary-line:`module-pw_build` for other GN templates available
10     within Pigweed.
11   - :bdg-ref-primary-line:`docs-build-system` for a high level guide and
12     background information on Pigweed's build system as a whole.
13
14Pigweed uses a custom GN-based build system to manage its Python code. The
15Pigweed Python build supports packaging, installation and distribution of
16interdependent local Python packages. It also provides for fast, incremental
17static analysis and test running suitable for live use during development (e.g.
18with :ref:`module-pw_watch`) or in continuous integration.
19
20Pigweed's Python code is exclusively managed by GN, but the GN-based build may
21be used alongside CMake, Bazel, or any other build system. Pigweed's environment
22setup uses GN to set up the initial Python environment, regardless of the final
23build system. As needed, non-GN projects can declare just their Python packages
24in GN.
25
26How it Works
27============
28In addition to compiler commands a Pigweed GN build will execute Python scripts
29for various reasons including running tests, linting code, generating protos and
30more. All these scripts are run as part of a
31:ref:`module-pw_build-pw_python_action` GN template which will ultimately run
32``python``. Running Python on it's own by default will make any Python packages
33installed on the users system available for importing. This is not good and can
34lead to flaky builds when different packages are installed on each developer
35workstation. To get around this the Python community uses `virtual environments
36<https://docs.python.org/3/library/venv.html>`_ (venvs) that expose a specific
37set of Python packages separate from the host system.
38
39When a Pigweed GN build starts a single venv is created for use by all
40:ref:`pw_python_actions <module-pw_build-pw_python_action>` throughout the build
41graph. Once created, all required third-party Python packages needed for the
42project are installed. At that point no further modifications are made to
43the venv. Of course if a new third-party package dependency is added it will be
44installed too. Beyond that all venvs remain static. More venvs can be created
45with the :ref:`module-pw_build-pw_python_venv` template if desired, but only one
46is used by default.
47
48.. card::
49
50   **Every pw_python_action is run inside a venv**
51   ^^^
52   .. mermaid::
53      :caption:
54
55      flowchart LR
56        out[GN Build Dir<br/>fa:fa-folder out]
57
58        out -->|ninja -C out| createvenvs
59
60        createvenvs(Create venvs)
61        createvenvs --> pyactions1
62        createvenvs --> pyactions2
63
64        subgraph pyactions1[Python venv 1]
65          direction TB
66          venv1(fa:fa-folder out/python-venv &nbsp)
67          a1["pw_python_action('one')"]
68          a2["pw_python_action('two')"]
69          venv1 --> a1
70          venv1 --> a2
71        end
72
73        subgraph pyactions2[Python venv 2]
74          direction TB
75          venv2(fa:fa-folder out/another-venv &nbsp)
76          a3["pw_python_action('three')"]
77          a4["pw_python_action('four')"]
78          venv2 --> a3
79          venv2 --> a4
80        end
81
82.. note::
83
84   Pigweed uses `this venv target
85   <https://cs.opensource.google/pigweed/pigweed/+/main:pw_env_setup/BUILD.gn?q=pigweed_build_venv>`_
86   if a project does not specify it's own build venv. See
87   :bdg-ref-primary-line:`docs-python-build-python-gn-venv` on how to define
88   your own default venv.
89
90Having a static venv containing only third-party dependencies opens the flood
91gates for python scripts to run. If the venv only contains third-party
92dependencies you may be wondering how you can import your own in-tree Python
93packages. Python code run in the build may still import any in-tree Python
94packages created with :ref:`module-pw_build-pw_python_package`
95templates. However this only works if a correct ``python_deps`` arg is
96provided. Having that Python dependency defined in GN allows the
97:ref:`module-pw_build-pw_python_action`
98to set `PYTHONPATH
99<https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH>`_ so that given
100package can be imported. This has the benefit of the build failing if a
101dependency for any Python action or package is missing.
102
103.. admonition:: Benefits of Python ``venvs`` in GN
104   :class: important
105
106   - Using venvs to execute Python in GN provides reproducible builds with fixed
107     third-party dependencies.
108   - Using ``PYTHONPATH`` coupled with ``python_deps`` to import in-tree Python
109     packages enforces dependency correctness.
110
111
112Managing Python Requirements
113============================
114
115.. _docs-python-build-python-gn-venv:
116
117Build Time Python Virtualenv
118----------------------------
119Pigweed's GN Python build infrastructure relies on `Python virtual environments
120<https://docs.python.org/3/library/venv.html>`_ for executing Python code. This
121provides a controlled isolated environment with a defined set of third party
122Python constraints where all Python tests, linting and
123:ref:`module-pw_build-pw_python_action` targets are executed.
124
125There must be at least one venv for Python defined in GN. There can be multiple
126venvs but one must be the designated default.
127
128The default build venv is specified via a GN arg and is best set in the root
129``.gn`` or ``BUILD.gn`` file. For example:
130
131.. code-block::
132
133   pw_build_PYTHON_BUILD_VENV = "//:project_build_venv"
134
135.. tip::
136   Additional :ref:`module-pw_build-pw_python_venv` targets can be created as
137   needed. The :ref:`module-pw_build-pw_python_action` template can take an
138   optional ``venv`` argument to specify which Python venv it should run
139   within. If not specified the target referred in the
140   ``pw_build_PYTHON_BUILD_VENV`` is used.
141
142.. _docs-python-build-python-gn-requirements-files:
143
144Third-party Python Requirements and Constraints
145-----------------------------------------------
146Your project may have third party Python dependencies you wish to install into
147the bootstrapped environment and in the GN build venv. There are two main ways
148to add Python package dependencies:
149
150**Adding Requirements Files**
151
1521. Add a ``install_requires`` entry to a ``setup.cfg`` file defined in a
153   :ref:`module-pw_build-pw_python_package` template. This is the best option
154   if your in-tree Python package requires an external Python package.
155
1562. Create a standard Python ``requirements.txt`` file in your project and add it
157   to the ``pw_build_PIP_REQUIREMENTS`` GN arg list.
158
159   Requirements files support a wide range of install locations including
160   packages from pypi.org, the local file system and git repos. See `pip's
161   Requirements File documentation
162   <https://pip.pypa.io/en/stable/user_guide/#requirements-files>`_ for more
163   info.
164
165   The GN arg can be set in your project's root ``.gn`` or ``BUILD.gn`` file.
166
167   .. code-block::
168
169      pw_build_PIP_REQUIREMENTS = [
170        # Project specific requirements
171        "//tools/requirements.txt",
172      ]
173
174   See the :ref:`docs-python-build-python-gn-structure` section below for a full
175   code listing.
176
177**Adding Constraints Files**
178
179Every project should ideally inherit Pigweed's third party Python package
180version. This is accomplished via `Python constraints files
181<https://pip.pypa.io/en/stable/user_guide/#constraints-files>`_. Constraints
182control which versions of packages get installed by ``pip`` if that package is
183installed. To inherit Pigweed's Python constraints include ``constraint.list``
184from the ``pw_env_setup`` module from in your top level ``.gn`` file. Additonal
185project specific constraints can be appended to this list.
186
187.. code-block::
188
189   pw_build_PIP_CONSTRAINTS = [
190     "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list",
191     "//tools/constraints.txt",
192   ]
193
194In-tree ``pw_python_package`` Requirements
195------------------------------------------
196A given venv inherits a project's requirements and constraint files by default
197via the ``pw_build_PIP_CONSTRAINTS`` and ``pw_build_PIP_REQUIREMENTS`` GN args
198as described above. This can be overridden if needed.
199
200``generated_requirements.txt``
201^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
202To ensure the requirements of in-tree :ref:`module-pw_build-pw_python_package`
203targets are installed :ref:`module-pw_build-pw_python_venv` introduces the
204``source_packages`` argument. This is a list of in-tree ``pw_python_package``
205GN targets expected to be used within the venv. When the venv is created each
206``pw_python_package``'s ``setup.cfg`` file is read to pull the
207``install_requires`` section for all third party dependencies. The full list of
208all in-tree packages and any in-tree transitive dependencies is then written to
209the out directory in a single ``generated_requirements.txt``.
210
211Take the ``//pw_build/py/gn_tests:downstream_tools_build_venv`` example below,
212its ``source package`` is a single ``pw_python_distribution`` package which
213bundles the ``pw_env_setup`` and ``pw_console`` ``pw_python_package``s. Those
214two packages each depend on a few other ``pw_python_package`` targets. The
215output ``generated_requirements.txt`` below merges all these package deps and
216adds ``-c`` lines for constraint files.
217
218.. seealso::
219   The pip documentation on the `Requirements File Format
220   <https://pip.pypa.io/en/stable/reference/requirements-file-format/#requirements-file-format>`_
221
222.. literalinclude:: pw_build/py/gn_tests/BUILD.gn
223   :start-after: [downstream-project-venv]
224   :end-before: [downstream-project-venv]
225
226.. code-block::
227   :caption: :octicon:`file;1em` out/python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/generated_requirements.txt
228   :name: generated_requirements
229
230   # Auto-generated requirements.txt from the following packages:
231   #
232   # //pw_arduino_build/py:py
233   # //pw_build/py/gn_tests:downstream_project_tools
234   # //pw_build/py:py
235   # //pw_cli/py:py
236   # //pw_console/py:py
237   # //pw_env_setup/py:py
238   # //pw_log_tokenized/py:py
239   # //pw_package/py:py
240   # //pw_presubmit/py:py
241   # //pw_stm32cube_build/py:py
242
243   # Constraint files:
244   -c ../../../../../../../pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
245
246   black>=23.1.0
247   build>=0.8.0
248   coloredlogs
249   coverage
250   ipython
251   jinja2
252   mypy>=0.971
253   parameterized
254   pip-tools>=6.12.3
255   prompt-toolkit>=3.0.26
256   psutil
257   ptpython>=3.0.20
258   pygments
259   pylint>=2.9.3
260   pyperclip
261   pyserial>=3.5,<4.0
262   pyyaml
263   setuptools
264   six
265   toml
266   types-pygments
267   types-pyserial>=3.5,<4.0
268   types-pyyaml
269   types-setuptools
270   types-six
271   websockets
272   wheel
273   yapf>=0.31.0
274
275``compiled_requirements.txt``
276^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
277The above ``generated_requirements.txt`` file is then fed into the
278``pip-compile`` command from `the pip-tools package
279<https://pypi.org/project/pip-tools>`_ to fully expand and pin each package with
280hashes. The resulting ``compiled_requirements.txt`` can then be used as the
281single Python requirements file for replicating this ``pw_python_venv``
282elsewhere. Each ``pw_python_venv`` will get this single file containing the
283exact versions of each required Python package.
284
285.. tip::
286   The ``compiled_requirements.txt`` generated by a ``pw_python_venv`` is used
287   by the :ref:`module-pw_build-pw_python_zip_with_setup` template when
288   producing a self contained zip of in-tree and third party Python packages.
289
290Below is a snippet of the ``compiled_requirements.txt`` for this
291:ref:`module-pw_build-pw_python_venv` target:
292``//pw_build/py/gn_tests:downstream_tools_build_venv``
293
294.. code-block::
295   :caption: :octicon:`file;1em` out/python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/compiled_requirements.txt
296   :name: compiled_requirements
297
298   #
299   # This file is autogenerated by pip-compile with Python 3.11
300   # by the following command:
301   #
302   #    pip-compile --allow-unsafe --generate-hashes
303   #      --output-file=python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/compiled_requirements.txt
304   #      --resolver=backtracking
305   #      python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/generated_requirements.txt
306   #
307   appdirs==1.4.4 \
308       --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \
309       --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
310       # via
311       #   -c python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/../../../../../../../pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
312       #   ptpython
313   astroid==2.14.2 \
314       --hash=sha256:0e0e3709d64fbffd3037e4ff403580550f14471fd3eaae9fa11cc9a5c7901153 \
315       --hash=sha256:a3cf9f02c53dd259144a7e8f3ccd75d67c9a8c716ef183e0c1f291bc5d7bb3cf
316       # via
317       #   -c python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/../../../../../../../pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
318       #   pylint
319   ...
320
321The presence of hashes in the above example can be controlled via the
322``pip_generate_hashes`` arg to the :ref:`module-pw_build-pw_python_venv`
323template.
324
325Caching Python Packages for Offline Installation
326------------------------------------------------
327
328.. _docs-python-build-downloading-packages:
329
330Downloading Packages
331^^^^^^^^^^^^^^^^^^^^
332The :ref:`module-pw_build-pw_python_venv` target adds an optional sub target
333that will download all Python packages from remote servers into a local
334directory. The remote server is typically `pypi.org <https://pypi.org/>`_.
335
336Taking the ``//pw_build/py/gn_tests:downstream_tools_build_venv`` target as an
337example again let's build a local cache. To run the download target append
338``.vendor_wheels`` to the end of the ``pw_python_venv`` target name. In this
339example it would be
340``//pw_build/py/gn_tests:downstream_tools_build_venv.vendor_wheels``
341
342To build that one gn target with ninja, pass the output name from gn as a target
343name for ninja:
344
345.. code-block:: bash
346
347   gn gen out
348   ninja -C out \
349     $(gn ls out --as=output \
350       '//pw_build/py/gn_tests:downstream_tools_build_venv.vendor_wheels')
351
352This creates a ``wheels`` folder with all downloaded packages and a
353``pip_download_log.txt`` with verbose logs from running ``pip download``.
354
355.. code-block::
356   :caption: :octicon:`file-directory;1em` Vendor wheels output directory
357   :name: vendor-wheel-output
358
359   out/python/gen/pw_build/py/gn_tests/downstream_tools_build_venv.vendor_wheels/
360   ├── pip_download_log.txt
361   └── wheels
362       ├── appdirs-1.4.4-py2.py3-none-any.whl
363       ├── astroid-2.14.2-py3-none-any.whl
364       ├── backcall-0.2.0-py2.py3-none-any.whl
365       ├── black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
366       ├ ...
367       ├── websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl
368       ├── wheel-0.40.0-py3-none-any.whl
369       ├── wrapt-1.14.1.tar.gz
370       └── yapf-0.31.0-py2.py3-none-any.whl
371
372Note the above output has both Python wheel ``.whl`` and source distribution
373``.tar.gz`` files. The ``.whl`` may contain Python packages with precompiled C
374extensions. This is denoted by this part of the filename:
375``cp311-cp311-manylinux_2_17_x86_64.whl``. These binary packages are selected by
376the ``pip download`` command based on the host machine python version, OS, and
377CPU architecture.
378
379.. warning::
380   If you need to cache Python packages for multiple platforms the
381   ``.vendor_wheels`` target will need to be run for each combination of Python
382   version, host operating system and architecture. For example, look at `the
383   files available for numpy <https://pypi.org/project/cffi/#files>`_. Some
384   combinations are:
385
386   - cp311, manylinux_2_17_x86_64
387   - cp311, manylinux2014_x86_64
388   - cp311, macosx_11_0_arm64
389   - cp311, macosx_10_9_x86_64
390   - cp311, win_amd64
391   - cp311, win32
392
393   Plus all of the above duplicated for Python 3.10 and 3.9 (``cp310`` and
394   ``cp39``).
395
396   The output of multiple ``.vendor_wheels`` runs on different host systems can
397   all be merged into the same output directory.
398
399``.vendor_wheels`` can attempt to download binary packages for multiple
400platforms all at once by setting a GN arg:
401
402.. code-block::
403
404   pw_build_PYTHON_PIP_DOWNLOAD_ALL_PLATFORMS = true
405
406This will invoke `pip download
407<https://pip.pypa.io/en/stable/cli/pip_download/>`_ for each combination of
408platform, architecture and Python version. This can take a significant amount of
409time to complete. The current set of combinations is shown below:
410
411.. literalinclude:: pw_build/py/pw_build/generate_python_wheel_cache.py
412   :start-after: [wheel-platform-args]
413   :end-before: [wheel-platform-args]
414
415.. warning::
416   The set of Python packages that will be downloaded is determined by the
417   ``compiled_requirements.txt`` file. This file can only be generated for the
418   current host OS and Python version. `pip-tools
419   <https://pypi.org/project/pip-tools>`_ does not expand requirements for
420   `platform specific dependencies
421   <https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#platform-specific-dependencies>`_. For
422   example ipython defines these two requirements:
423
424   .. code-block::
425
426      appnope; sys_platform == "darwin"
427      colorama; sys_platform == "win32"
428
429   If pip-tools is run on Linux then the above packages will not appear in
430   ``compiled_requirements.txt`` and not downloaded by the ``.vendor_wheels``
431   target.
432
433.. _docs-python-build-installing-offline:
434
435Installing Offline
436^^^^^^^^^^^^^^^^^^
437Once the vendor wheel output is saved to a directory in your project you can use
438this as the default pip install location in two different ways.
439
440GN Args
441.......
442Setting these args in the ``//.gn`` file will add the relevant pip command line
443args to perform offline installations.
444
445.. code-block::
446
447   # Adds --no-index forcing pip to not reach out to the internet (pypi.org) to
448   # download packages. Using this option requires setting
449   # pw_build_PYTHON_PIP_INSTALL_FIND_LINKS as well.
450   pw_build_PYTHON_PIP_INSTALL_OFFLINE = true
451
452   # List of paths to folders containing Python wheels (*.whl) or source tar
453   # files (*.tar.gz). Pip will check each of these directories when looking for
454   # potential install candidates.
455   pw_build_PYTHON_PIP_INSTALL_FIND_LINKS = [
456     "//environment/cipd/packages/python_packages/universal",
457     "//environment/cipd/packages/python_packages/linux/cp311",
458   ]
459
460   # Optional: Adds '--no-cache-dir' forcing pip to ignore any previously cached
461   # Python packages. On most systems this is located in ~/.cache/pip/
462   pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE = false
463
464Using a ``.pip.conf`` File
465..........................
4661. Create a ``//pip.conf`` file containing:
467
468   .. code-block::
469      :caption: :octicon:`file;1em` //pip.conf
470      :name: pip-conf-file
471
472      [global]
473      # Disable searching pypi.org for packages
474      no-index = True
475      # Find packages in these directories:
476      find-links =
477          file://third_party/python_packages/universal
478          file://third_party/python_packages/linux/cp311
479
480   This tells pip to not search pypi.org for packages and only look in
481   ``third_party/python_packages/universal`` and
482   ``third_party/python_packages/linux/cp311``. These paths can be absolute or
483   are relative to the ``pip.conf`` file.
484
4852. In the project ``bootstrap.sh`` set ``PIP_CONFIG_FILE`` to the location of
486   this file.
487
488   .. code-block:: bash
489
490      export PIP_CONFIG_FILE="${PW_PROJECT_ROOT}/pip.conf"
491
492   With that environment var set all invocations of pip will apply the config file
493   settings above.
494
495.. seealso::
496   The ``pip`` `documentation on Configuration
497   <https://pip.pypa.io/en/stable/topics/configuration/>`_.
498
499.. _docs-python-build-python-gn-structure:
500
501GN File Structure for Python Code
502=================================
503Here is a full example of what is required to build Python packages using
504Pigweed's GN build system. A brief file hierarchy is shown here with file
505content following. See also :ref:`docs-python-build-structure` below for details
506on the structure of Python packages.
507
508.. code-block::
509   :caption: :octicon:`file-directory;1em` Top level GN file hierarchy
510   :name: gn-python-file-tree
511
512   project_root/
513   ├── .gn
514   ├── BUILDCONFIG.gn
515   ├── build_overrides/
516   │   └── pigweed.gni
517   ├── BUILD.gn
518519   ├── python_package1/
520   │   ├── BUILD.gn
521   │   ├── setup.cfg
522   │   ├── pyproject.toml
523   │   │
524   │   ├── package_name/
525   │   │   ├── module_a.py
526   │   │   ├── module_b.py
527   │   │   ├── py.typed
528   │   │   └── nested_package/
529   │   │       ├── py.typed
530   │   │       └── module_c.py
531   │   │
532   │   ├── module_a_test.py
533   │   └── module_c_test.py
534535   ├── third_party/
536   │   └── pigweed/
537538   └── ...
539
540- :octicon:`file-directory;1em` project_root/
541
542  - :octicon:`file;1em` .gn
543
544    .. code-block::
545
546       buildconfig = "//BUILDCONFIG.gn"
547       import("//build_overrides/pigweed.gni")
548
549       default_args = {
550         pw_build_PIP_CONSTRAINTS = [
551           # Inherit Pigweed Python constraints
552           "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list",
553
554           # Project specific constraints file
555           "//tools/constraint.txt",
556         ]
557
558         pw_build_PIP_REQUIREMENTS = [
559           # Project specific requirements
560           "//tools/requirements.txt",
561         ]
562
563         # Default gn build virtualenv target.
564         pw_build_PYTHON_BUILD_VENV = "//:project_build_venv"
565       }
566
567    .. tip::
568
569       There are some additional gn args to control how pip installations are
570       performed during the build.
571
572       .. literalinclude:: pw_build/python_gn_args.gni
573          :start-after: [default-pip-gn-args]
574          :end-before: [default-pip-gn-args]
575
576  - :octicon:`file;1em` BUILDCONFIG.gn
577
578    .. code-block::
579
580       _pigweed_directory = {
581         import("//build_overrides/pigweed.gni")
582       }
583
584       set_default_toolchain("${_pigweed_directory.dir_pw_toolchain}/default")
585
586  - :octicon:`file-directory;1em` build_overrides / :octicon:`file;1em` pigweed.gni
587
588    .. code-block::
589
590       declare_args() {
591         # Location of the Pigweed repository.
592         dir_pigweed = "//third_party/pigweed/"
593       }
594
595       # Upstream Pigweed modules.
596       import("$dir_pigweed/modules.gni")
597
598  - :octicon:`file;1em` BUILD.gn
599
600    .. code-block::
601
602       import("//build_overrides/pigweed.gni")
603
604       import("$dir_pw_build/python.gni")
605       import("$dir_pw_build/python_dist.gni")
606       import("$dir_pw_build/python_venv.gni")
607       import("$dir_pw_unit_test/test.gni")
608
609       # Lists all the targets build by default with e.g. `ninja -C out`.
610       group("default") {
611         deps = [
612           ":python.lint",
613           ":python.tests",
614         ]
615       }
616
617       # This group is built during bootstrap to setup the interactive Python
618       # environment.
619       pw_python_group("python") {
620         python_deps = [
621           # Generate and pip install _all_python_packages
622           ":pip_install_project_tools",
623         ]
624       }
625
626       # In-tree Python packages
627       _project_python_packages = [
628         "//python_package1",
629       ]
630
631       # Pigweed Python packages to include
632       _pigweed_python_packages = [
633         "$dir_pw_env_setup:core_pigweed_python_packages",
634         "$dir_pigweed/targets/lm3s6965evb_qemu/py",
635         "$dir_pigweed/targets/stm32f429i_disc1/py",
636       ]
637
638       _all_python_packages =
639           _project_python_packages + _pigweed_python_packages
640
641       # The default venv for Python actions in GN
642       # Set this gn arg in a declare_args block in this file 'BUILD.gn' or in '.gn' to
643       # use this venv.
644       #
645       #   pw_build_PYTHON_BUILD_VENV = "//:project_build_venv"
646       #
647       pw_python_venv("project_build_venv") {
648         path = "$root_build_dir/python-venv"
649         constraints = pw_build_PIP_CONSTRAINTS
650         requirements = pw_build_PIP_REQUIREMENTS
651
652         # Ensure all third party Python dependencies are installed into this venv.
653         # This works by checking the setup.cfg files for all packages listed here and
654         # installing the packages listed in the [options].install_requires field.
655         source_packages = _all_python_packages
656       }
657
658       # This template collects all python packages and their dependencies into a
659       # single super Python package for installation into the bootstrapped virtual
660       # environment.
661       pw_python_distribution("generate_project_python_distribution") {
662         packages = _all_python_packages
663         generate_setup_cfg = {
664           name = "project-tools"
665           version = "0.0.1"
666           append_date_to_version = true
667           include_default_pyproject_file = true
668         }
669       }
670
671       # Install the project-tools super Python package into the bootstrapped
672       # Python venv.
673       pw_python_pip_install("pip_install_project_tools") {
674         packages = [ ":generate_project_python_distribution" ]
675       }
676
677.. _docs-python-build-structure:
678
679Pigweed Module Structure for Python Code
680========================================
681Pigweed Python code is structured into standard Python packages. This makes it
682simple to package and distribute Pigweed Python packages with common Python
683tools.
684
685Like all Pigweed source code, Python packages are organized into Pigweed
686modules. A module's Python package is nested under a ``py/`` directory (see
687:ref:`Pigweed Module Stucture <docs-module-structure>`).
688
689.. code-block::
690   :caption: :octicon:`file-directory;1em` Example layout of a Pigweed Python package.
691   :name: python-file-tree
692
693   module_name/
694   ├── py/
695   │   ├── BUILD.gn
696   │   ├── setup.cfg
697   │   ├── pyproject.toml
698   │   ├── package_name/
699   │   │   ├── module_a.py
700   │   │   ├── module_b.py
701   │   │   ├── py.typed
702   │   │   └── nested_package/
703   │   │       ├── py.typed
704   │   │       └── module_c.py
705   │   ├── module_a_test.py
706   │   └── module_c_test.py
707   └── ...
708
709The ``BUILD.gn`` declares this package in GN. For upstream Pigweed, a presubmit
710check in ensures that all Python files are listed in a ``BUILD.gn``.
711
712Pigweed prefers to define Python packages using ``setup.cfg`` files. In the
713above file tree the ``pyproject.toml`` file is only a stub with the following
714content:
715
716.. code-block::
717   :caption: :octicon:`file;1em` pyproject.toml
718   :name: pyproject-toml-stub
719
720   [build-system]
721   requires = ['setuptools', 'wheel']
722   build-backend = 'setuptools.build_meta'
723
724Each ``pyproject.toml`` file is required to specify which build system should be
725used for the given Python package. In Pigweed's case it always specifies using
726setuptools.
727
728.. seealso::
729
730   - ``setup.cfg`` examples at `Configuring setup() using setup.cfg files`_
731   - ``pyproject.toml`` background at `Build System Support - How to use it?`_
732
733.. _module-pw_build-python-target:
734
735pw_python_package targets
736-------------------------
737The key abstraction in the Python build is the ``pw_python_package``.
738A ``pw_python_package`` represents a Python package as a GN target. It is
739implemented with a GN template. The ``pw_python_package`` template is documented
740in :ref:`module-pw_build-python`.
741
742The key attributes of a ``pw_python_package`` are
743
744- a ``setup.cfg`` and ``pyproject.toml`` file,
745- source files,
746- test files,
747- dependencies on other ``pw_python_package`` targets.
748
749A ``pw_python_package`` target is composed of several GN subtargets. Each
750subtarget represents different functionality in the Python build.
751
752- ``<name>`` - Represents the Python files in the build, but does not take any
753  actions. All subtargets depend on this target.
754- ``<name>.tests`` - Runs all tests for this package.
755
756  - ``<name>.tests.<test_file>`` - Runs the specified test.
757
758- ``<name>.lint`` - Runs static analysis tools on the Python code. This is a
759  group of three subtargets:
760
761  - ``<name>.lint.mypy`` - Runs Mypy on all Python files, if enabled.
762  - ``<name>.lint.pylint`` - Runs Pylint on all Python files, if enabled.
763  - ``<name>.lint.ruff`` - Runs ruff on all Python files, if enabled.
764
765- ``<name>.install`` - Installs the package in a Python virtual environment.
766- ``<name>.wheel`` - Builds a Python wheel for this package.
767
768To avoid unnecessary duplication, all Python actions are executed in the default
769toolchain, even if they are referred to from other toolchains.
770
771Testing
772^^^^^^^
773Tests for a Python package are listed in its ``pw_python_package`` target.
774Adding a new test is simple: write the test file and list it in its accompanying
775Python package. The build will run it when the test, the package, or one of its
776dependencies is updated.
777
778Static analysis
779^^^^^^^^^^^^^^^
780``pw_python_package`` targets are preconfigured to run Pylint, Mypy and Ruff on
781their source and test files. Users may specify which ``pylintrc``, ``mypy_ini``
782and ``ruff_toml`` files to use on a per-package basis. The configuration files
783may also be provided in the directory structure; the tools will locate them
784using their standard means. Like tests, static analysis is only run when files
785or their dependencies change.
786
787Packages may opt out of static analysis as necessary by setting
788``static_analysis`` on the ``pw_python_package`` target.
789
790The default set of analysis tools to run can be set globally via a GN arg
791``pw_build_PYTHON_STATIC_ANALYSIS_TOOLS``. By default this is set to include the
792below tools:
793
794.. literalinclude:: pw_build/python.gni
795   :start-after: [python-static-analysis-tools]
796   :end-before: [python-static-analysis-tools]
797
798In addition to user specified ``mypy_ini`` files some arguments are always
799passed to ``mypy`` by default. They can be seen in this excerpt of
800``//pw_build/python.gni`` below:
801
802.. literalinclude:: pw_build/python.gni
803   :start-after: [default-mypy-args]
804   :end-before: [default-mypy-args]
805
806Building Python wheels
807^^^^^^^^^^^^^^^^^^^^^^
808`Wheels <https://wheel.readthedocs.io/en/stable/>`_ are the standard format for
809distributing Python packages. The Pigweed Python build supports creating wheels
810for individual packages and groups of packages. Building the ``.wheel``
811subtarget creates a ``.whl`` file for the package using the PyPA's `build
812<https://pypa-build.readthedocs.io/en/stable/>`_ tool.
813
814The ``.wheel`` subtarget of any ``pw_python_package`` or
815:ref:`module-pw_build-pw_python_distribution` records the location of the
816generated wheel with `GN metadata
817<https://gn.googlesource.com/gn/+/HEAD/docs/reference.md#var_metadata>`_.
818Wheels for a Python package and its transitive dependencies can be collected
819from the ``pw_python_package_wheels`` key. See
820:ref:`module-pw_build-python-dist`.
821
822Protocol buffers
823^^^^^^^^^^^^^^^^
824The Pigweed GN build supports protocol buffers with the ``pw_proto_library``
825target (see :ref:`module-pw_protobuf_compiler`). Python protobuf modules are
826generated as standalone Python packages by default. Protocol buffers may also be
827nested within existing Python packages. In this case, the Python package in the
828source tree is incomplete; the final Python package, including protobufs, is
829generated in the output directory.
830
831Generating setup.cfg
832^^^^^^^^^^^^^^^^^^^^
833The ``pw_python_package`` target in the ``BUILD.gn`` duplicates much of the
834information in the ``setup.cfg`` file. In many cases, it would be possible to
835generate a ``setup.cfg`` file rather than including it in the source
836tree. However, removing the ``setup.cfg`` would preclude using a direct,
837editable installation from the source tree.
838
839Pigweed packages containing protobufs are generated in full or in part. These
840packages may use generated setup files, since they are always packaged or
841installed from the build output directory.
842
843
844Rationale
845=========
846
847Background
848----------
849Developing software involves much more than writing source code. Software needs
850to be compiled, executed, tested, analyzed, packaged, and deployed. As projects
851grow beyond a few files, these tasks become impractical to manage manually.
852Build systems automate these auxiliary tasks of software development, making it
853possible to build larger, more complex systems quickly and robustly.
854
855Python is an interpreted language, but it shares most build automation concerns
856with other languages. Pigweed uses Python extensively and must address these
857needs for itself and its users.
858
859Existing solutions
860------------------
861The Python programming langauge does not have an official build automation
862system. However, there are numerous Python-focused build automation tools with
863varying degrees of adoption. See the `Python Wiki
864<https://wiki.python.org/moin/ConfigurationAndBuildTools>`_ for examples.
865
866A few Python tools have become defacto standards, including `setuptools
867<https://pypi.org/project/setuptools/>`_, `wheel
868<https://pypi.org/project/wheel/>`_, and `pip <https://pypi.org/project/pip/>`_.
869These essential tools address key aspects of Python packaging and distribution,
870but are not intended for general build automation. Tools like `PyBuilder
871<https://pybuilder.io/>`_ and `tox <https://tox.readthedocs.io/en/latest/>`_
872provide more general build automation for Python.
873
874The `Bazel <http://bazel.build/>`_ build system has first class support for
875Python and other languages used by Pigweed, including protocol buffers.
876
877Challenges
878----------
879Pigweed's use of Python is different from many other projects. Pigweed is a
880multi-language, modular project. It serves both as a library or middleware and
881as a development environment.
882
883This section describes Python build automation challenges encountered by
884Pigweed.
885
886Dependencies
887^^^^^^^^^^^^
888Pigweed is organized into distinct modules. In Python, each module is a separate
889package, potentially with dependencies on other local or `PyPI
890<https://pypi.org/>`_ packages.
891
892The basic Python packaging tools lack dependency tracking for local packages.
893For example, a package's ``setup.cfg`` lists all of its dependencies, but
894``pip`` is not aware of local packages until they are installed. Packages must
895be installed with their dependencies taken into account, in topological sorted
896order.
897
898To work around this, one could set up a private `PyPI server
899<https://pypi.org/project/pypiserver/>`_ instance, but this is too cumbersome
900for daily development and incompatible with editable package installation.
901
902Testing
903^^^^^^^
904Tests are crucial to having a healthy, maintainable codebase. While they take
905some initial work to write, the time investment pays for itself many times over
906by contributing to the long-term resilience of a codebase. Despite their
907benefit, developers don't always take the time to write tests. Any barriers to
908writing and running tests result in fewer tests and consequently more fragile,
909bug-prone codebases.
910
911There are lots of great Python libraries for testing, such as
912`unittest <https://docs.python.org/3/library/unittest.html>`_ and
913`pytest <https://docs.pytest.org/en/stable/>`_. These tools make it easy to
914write and execute individual Python tests, but are not well suited for managing
915suites of interdependent tests in a large project. Writing a test with these
916utilities does not automatically run them or keep running them as the codebase
917changes.
918
919Static analysis
920^^^^^^^^^^^^^^^
921
922.. seealso::
923
924   :bdg-ref-primary-line:`docs-automated-analysis` for info on other static
925   analysis tools used in Pigweed.
926
927Various static analysis tools exist for Python. Two widely used, powerful tools
928are `Pylint <https://www.pylint.org/>`_ and `Mypy <http://mypy-lang.org/>`_.
929Using these tools improves code quality, as they catch bugs, encourage good
930design practices, and enforce a consistent coding style. As with testing,
931barriers to running static analysis tools cause many developers to skip them.
932Some developers may not even be aware of these tools.
933
934Deploying static analysis tools to a codebase like Pigweed has some challenges.
935Mypy and Pylint are simple to run, but they are extremely slow. Ideally, these
936tools would be run constantly during development, but only on files that change.
937These tools do not have built-in support for incremental runs or dependency
938tracking.
939
940Another challenge is configuration. Mypy and Pylint support using configuration
941files to select which checks to run and how to apply them. Both tools only
942support using a single configuration file for an entire run, which poses a
943challenge to modular middleware systems where different parts of a project may
944require different configurations.
945
946Protocol buffers
947^^^^^^^^^^^^^^^^
948`Protocol buffers <https://developers.google.com/protocol-buffers>`_ are an
949efficient system for serializing structured data. They are widely used by Google
950and other companies.
951
952The protobuf compiler ``protoc`` generates Python modules from ``.proto`` files.
953``protoc`` strictly generates protobuf modules according to their directory
954structure. This works well in a monorepo, but poses a challenge to a middleware
955system like Pigweed. Generating protobufs by path also makes integrating
956protobufs with existing packages awkward.
957
958Requirements
959------------
960Pigweed aims to provide high quality software components and a fast, effective,
961flexible development experience for its customers. Pigweed's high-level goals
962and the `challenges`_ described above inform these requirements for the Pigweed
963Python build.
964
965- Integrate seamlessly with the other Pigweed build tools.
966- Easy to use independently, even if primarily using a different build system.
967- Support standard packaging and distribution with setuptools, wheel, and pip.
968- Correctly manage interdependent local Python packages.
969- Out-of-the-box support for writing and running tests.
970- Preconfigured, trivial-to-run static analysis integration for Pylint and Mypy.
971- Fast, dependency-aware incremental rebuilds and test execution, suitable for
972  use with :ref:`module-pw_watch`.
973- Seamless protocol buffer support.
974
975Design Decision
976---------------
977Existing Python tools may be effective for Python codebases, but their utility
978is more limited in a multi-language project like Pigweed. The cost of bringing
979up and maintaining an additional build automation system for a single language
980is high.
981
982Pigweed uses GN as its primary build system for all languages. While GN does not
983natively support Python, adding support is straightforward with GN templates.
984
985GN has strong multi-toolchain and multi-language capabilities. In GN, it is
986straightforward to share targets and artifacts between different languages. For
987example, C++, Go, and Python targets can depend on the same protobuf
988declaration. When using GN for multiple languages, Ninja schedules build steps
989for all languages together, resulting in faster total build times.
990
991Not all Pigweed users build with GN. Of Pigweed's three supported build systems,
992GN is the fastest, lightest weight, and easiest to run. It also has simple,
993clean syntax. This makes it feasible to use GN only for Python while building
994primarily with a different system.
995
996Given these considerations, GN is an ideal choice for Pigweed's Python build.
997
998.. _Configuring setup() using setup.cfg files: https://ipython.readthedocs.io/en/stable/interactive/reference.html#embedding
999.. _Build System Support - How to use it?: https://setuptools.readthedocs.io/en/latest/build_meta.html?highlight=pyproject.toml#how-to-use-it
1000