| .. _docs-python-build: |
| |
| ====================== |
| Pigweed's Python build |
| ====================== |
| Pigweed uses a custom GN-based build system to manage its Python code. The |
| Pigweed Python build supports packaging, installation, and distribution of |
| interdependent local Python packages. It also provides for fast, incremental |
| static analysis and test running suitable for live use during development (e.g. |
| with :ref:`module-pw_watch`) or in continuous integration. |
| |
| Pigweed's Python code is exclusively managed by GN, but the GN-based build may |
| be used alongside CMake, Bazel, or any other build system. Pigweed's environment |
| setup uses GN to set up the initial Python environment, regardless of the final |
| build system. As needed, non-GN projects can declare just their Python packages |
| in GN. |
| |
| Background |
| ========== |
| Developing software involves much more than writing source code. Software needs |
| to be compiled, executed, tested, analyzed, packaged, and deployed. As projects |
| grow beyond a few files, these tasks become impractical to manage manually. |
| Build systems automate these auxiliary tasks of software development, making it |
| possible to build larger, more complex systems quickly and robustly. |
| |
| Python is an interpreted language, but it shares most build automation concerns |
| with other languages. Pigweed uses Python extensively and must to address these |
| needs for itself and its users. |
| |
| Existing solutions |
| ================== |
| The Python programming langauge does not have an official build automation |
| system. However, there are numerous Python-focused build automation tools with |
| varying degrees of adoption. See the `Python Wiki |
| <https://wiki.python.org/moin/ConfigurationAndBuildTools>`_ for examples. |
| |
| A few Python tools have become defacto standards, including `setuptools |
| <https://pypi.org/project/setuptools/>`_, `wheel |
| <https://pypi.org/project/wheel/>`_, and `pip <https://pypi.org/project/pip/>`_. |
| These essential tools address key aspects of Python packaging and distribution, |
| but are not intended for general build automation. Tools like `PyBuilder |
| <https://pybuilder.io/>`_ and `tox <https://tox.readthedocs.io/en/latest/>`_ |
| provide more general build automation for Python. |
| |
| The `Bazel <http://bazel.build/>`_ build system has first class support for |
| Python and other languages used by Pigweed, including protocol buffers. |
| |
| Challenges |
| ========== |
| Pigweed's use of Python is different from many other projects. Pigweed is a |
| multi-language, modular project. It serves both as a library or middleware and |
| as a development environment. |
| |
| This section describes Python build automation challenges encountered by |
| Pigweed. |
| |
| Dependencies |
| ------------ |
| Pigweed is organized into distinct modules. In Python, each module is a separate |
| package, potentially with dependencies on other local or `PyPI |
| <https://pypi.org/>`_ packages. |
| |
| The basic Python packaging tools lack dependency tracking for local packages. |
| For example, a package's ``setup.py`` or ``setup.cfg`` lists all of |
| its dependencies, but ``pip`` is not aware of local packages until they are |
| installed. Packages must be installed with their dependencies taken into |
| account, in topological sorted order. |
| |
| To work around this, one could set up a private `PyPI server |
| <https://pypi.org/project/pypiserver/>`_ instance, but this is too cumbersome |
| for daily development and incompatible with editable package installation. |
| |
| Testing |
| ------- |
| Tests are crucial to having a healthy, maintainable codebase. While they take |
| some initial work to write, the time investment pays for itself many times over |
| by contributing to the long-term resilience of a codebase. Despite their |
| benefit, developers don't always take the time to write tests. Any barriers to |
| writing and running tests result in fewer tests and consequently more fragile, |
| bug-prone codebases. |
| |
| There are lots of great Python libraries for testing, such as |
| `unittest <https://docs.python.org/3/library/unittest.html>`_ and |
| `pytest <https://docs.pytest.org/en/stable/>`_. These tools make it easy to |
| write and execute individual Python tests, but are not well suited for managing |
| suites of interdependent tests in a large project. Writing a test with these |
| utilities does not automatically run them or keep running them as the codebase |
| changes. |
| |
| Static analysis |
| --------------- |
| Various static analysis tools exist for Python. Two widely used, powerful tools |
| are `Pylint <https://www.pylint.org/>`_ and `Mypy <http://mypy-lang.org/>`_. |
| Using these tools improves code quality, as they catch bugs, encourage good |
| design practices, and enforce a consistent coding style. As with testing, |
| barriers to running static analysis tools cause many developers to skip them. |
| Some developers may not even be aware of these tools. |
| |
| Deploying static analysis tools to a codebase like Pigweed has some challenges. |
| Mypy and Pylint are simple to run, but they are extremely slow. Ideally, these |
| tools would be run constantly during development, but only on files that change. |
| These tools do not have built-in support for incremental runs or dependency |
| tracking. |
| |
| Another challenge is configuration. Mypy and Pylint support using configuration |
| files to select which checks to run and how to apply them. Both tools only |
| support using a single configuration file for an entire run, which poses a |
| challenge to modular middleware systems where different parts of a project may |
| require different configurations. |
| |
| Protocol buffers |
| ---------------- |
| `Protocol buffers <https://developers.google.com/protocol-buffers>`_ are an |
| efficient system for serializing structured data. They are widely used by Google |
| and other companies. |
| |
| The protobuf compiler ``protoc`` generates Python modules from ``.proto`` files. |
| ``protoc`` strictly generates protobuf modules according to their directory |
| structure. This works well in a monorepo, but poses a challenge to a middleware |
| system like Pigweed. Generating protobufs by path also makes integrating |
| protobufs with existing packages awkward. |
| |
| Requirements |
| ============ |
| Pigweed aims to provide high quality software components and a fast, effective, |
| flexible development experience for its customers. Pigweed's high-level goals |
| and the `challenges`_ described above inform these requirements for the Pigweed |
| Python build. |
| |
| - Integrate seamlessly with the other Pigweed build tools. |
| - Easy to use independently, even if primarily using a different build system. |
| - Support standard packaging and distribution with setuptools, wheel, and pip. |
| - Correctly manage interdependent local Python packages. |
| - Out-of-the-box support for writing and running tests. |
| - Preconfigured, trivial-to-run static analysis integration for Pylint and Mypy. |
| - Fast, dependency-aware incremental rebuilds and test execution, suitable for |
| use with :ref:`module-pw_watch`. |
| - Seamless protocol buffer support. |
| |
| Detailed design |
| =============== |
| |
| Build automation tool |
| --------------------- |
| Existing Python tools may be effective for Python codebases, but their utility |
| is more limited in a multi-language project like Pigweed. The cost of bringing |
| up and maintaining an additional build automation system for a single language |
| is high. |
| |
| Pigweed uses GN as its primary build system for all languages. While GN does not |
| natively support Python, adding support is straightforward with GN templates. |
| |
| GN has strong multi-toolchain and multi-language capabilities. In GN, it is |
| straightforward to share targets and artifacts between different languages. For |
| example, C++, Go, and Python targets can depend on the same protobuf |
| declaration. When using GN for multiple languages, Ninja schedules build steps |
| for all languages together, resulting in faster total build times. |
| |
| Not all Pigweed users build with GN. Of Pigweed's three supported build systems, |
| GN is the fastest, lightest weight, and easiest to run. It also has simple, |
| clean syntax. This makes it feasible to use GN only for Python while building |
| primarily with a different system. |
| |
| Given these considerations, GN is an ideal choice for Pigweed's Python build. |
| |
| .. _docs-python-build-structure: |
| |
| Module structure |
| ---------------- |
| Pigweed Python code is structured into standard Python packages. This makes it |
| simple to package and distribute Pigweed Python packages with common Python |
| tools. |
| |
| Like all Pigweed source code, Python packages are organized into Pigweed |
| modules. A module's Python package is nested under a ``py/`` directory (see |
| :ref:`docs-module-structure`). |
| |
| .. code-block:: |
| |
| module_name/ |
| ├── py/ |
| │ ├── BUILD.gn |
| │ ├── setup.py |
| │ ├── package_name/ |
| │ │ ├── module_a.py |
| │ │ ├── module_b.py |
| │ │ ├── py.typed |
| │ │ └── nested_package/ |
| │ │ ├── py.typed |
| │ │ └── module_c.py |
| │ ├── module_a_test.py |
| │ └── module_c_test.py |
| └── ... |
| |
| The ``BUILD.gn`` declares this package in GN. For upstream Pigweed, a presubmit |
| check in ensures that all Python files are listed in a ``BUILD.gn``. |
| |
| .. _module-pw_build-python-target: |
| |
| pw_python_package targets |
| ------------------------- |
| The key abstraction in the Python build is the ``pw_python_package``. |
| A ``pw_python_package`` represents a Python package as a GN target. It is |
| implemented with a GN template. The ``pw_python_package`` template is documented |
| in :ref:`module-pw_build-python`. |
| |
| The key attributes of a ``pw_python_package`` are |
| |
| - a ``setup.py`` file, |
| - source files, |
| - test files, |
| - dependencies on other ``pw_python_package`` targets. |
| |
| A ``pw_python_package`` target is composed of several GN subtargets. Each |
| subtarget represents different functionality in the Python build. |
| |
| - ``<name>`` - Represents the Python files in the build, but does not take any |
| actions. All subtargets depend on this target. |
| - ``<name>.tests`` - Runs all tests for this package. |
| |
| - ``<name>.tests.<test_file>`` - Runs the specified test. |
| |
| - ``<name>.lint`` - Runs static analysis tools on the Python code. This is a |
| group of two subtargets: |
| |
| - ``<name>.lint.mypy`` - Runs Mypy on all Python files, if enabled. |
| - ``<name>.lint.pylint`` - Runs Pylint on all Python files, if enabled. |
| |
| - ``<name>.install`` - Installs the package in a Python virtual environment. |
| - ``<name>.wheel`` - Builds a Python wheel for this package. |
| |
| To avoid unnecessary duplication, all Python actions are executed in the default |
| toolchain, even if they are referred to from other toolchains. |
| |
| Testing |
| ^^^^^^^ |
| Tests for a Python package are listed in its ``pw_python_package`` target. |
| Adding a new test is simple: write the test file and list it in its accompanying |
| Python package. The build will run the it when the test, the package, or one |
| of its dependencies is updated. |
| |
| Static analysis |
| ^^^^^^^^^^^^^^^ |
| ``pw_python_package`` targets are preconfigured to run Pylint and Mypy on their |
| source and test files. Users may specify which ``pylintrc`` and ``mypy.ini`` |
| files to |
| use on a per-package basis. The configuration files may also be provided in the |
| directory structure; the tools will locate them using their standard means. Like |
| tests, static analysis is only run when files or their dependencies change. |
| |
| Packages may opt out of static analysis as necessary. |
| |
| Installing packages in a virtual environment |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| Python packages declared in the Python build may be installed in a specified |
| `virtual environment <https://docs.python.org/3/tutorial/venv.html>`_. The |
| default venv to use may be specified using a GN build arg. The venv may be |
| overridden for individual targets. The Python build tracks installation status |
| of packages based on which venv is in use. |
| |
| The Python build examines the ``VIRTUAL_ENV`` environment variable to determine |
| the current venv. If the selected virtual environment is active, packages are |
| installed directly into it. If the venv is not active, it is activated before |
| installing the packages. |
| |
| .. admonition:: Under construction |
| |
| Pigweed Python targets are not yet aware of the virtual environment. |
| Currently, all actions occur in the active venv without consulting |
| ``VIRTUAL_ENV``. |
| |
| Python packages defined entirely in tree are installed with the ``--editable`` |
| option. Partially or fully generated packages are installed without that option. |
| |
| Building Python wheels |
| ^^^^^^^^^^^^^^^^^^^^^^ |
| Wheels are the standard format for distributing Python packages. The Pigweed |
| Python build supports creating wheels for individual packages and groups of |
| packages. Building the ``.wheel`` subtarget creates a ``.whl`` file for the |
| package using the PyPA's `build <https://pypa-build.readthedocs.io/en/stable/>`_ |
| tool. |
| |
| The ``.wheel`` subtarget of ``pw_python_package`` records the location of |
| the generated wheel with `GN metadata |
| <https://gn.googlesource.com/gn/+/HEAD/docs/reference.md#var_metadata>`_. |
| Wheels for a Python package and its transitive dependencies can be collected |
| from the ``pw_python_package_wheels`` key. See |
| :ref:`module-pw_build-python-dist`. |
| |
| Protocol buffers |
| ^^^^^^^^^^^^^^^^ |
| The Pigweed GN build supports protocol buffers with the ``pw_proto_library`` |
| target (see :ref:`module-pw_protobuf_compiler`). Python protobuf modules are |
| generated as standalone Python packages by default. Protocol buffers may also be |
| nested within existing Python packages. In this case, the Python package in the |
| source tree is incomplete; the final Python package, including protobufs, is |
| generated in the output directory. |
| |
| Generating setup.py |
| ------------------- |
| The ``pw_python_package`` target in the ``BUILD.gn`` duplicates much of the |
| information in the ``setup.py`` or ``setup.cfg`` file. In many cases, it would |
| be possible to generate a ``setup.py`` file rather than including it in the |
| source tree. However, removing the ``setup.py`` would preclude using a direct, |
| editable installation from the source tree. |
| |
| Pigweed packages containing protobufs are generated in full or in part. These |
| packages may use generated setup files, since they are always be packaged or |
| installed from the build output directory. |
| |
| See also |
| ======== |
| |
| - :ref:`module-pw_build-python` |
| - :ref:`module-pw_build` |
| - :ref:`docs-build-system` |