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  ) 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  ) 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 518 │ 519 ├── 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 534 │ 535 ├── third_party/ 536 │ └── pigweed/ 537 │ 538 └── ... 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