|
| 1 | +.. SPDX-FileCopyrightText: 2023 The meson-python developers |
| 2 | +.. |
| 3 | +.. SPDX-License-Identifier: MIT |
| 4 | +
|
| 5 | +.. _how-to-guides-dynamic-versioning: |
| 6 | + |
| 7 | +****************** |
| 8 | +Dynamic versioning |
| 9 | +****************** |
| 10 | + |
| 11 | +The most common approach to versioning is to keep a static version number in |
| 12 | +``pyproject.toml`` only, and update it before a new release in a regular commit. |
| 13 | +This is simple and robust. However, sometimes a package author may want more |
| 14 | +from versioning, and hence reach for dynamic versioning. E.g.: |
| 15 | + |
| 16 | +1. Use the package version in a ``meson.build`` file without duplicating the version string between ``pyproject.toml`` and ``meson.build``. |
| 17 | +2. Use the hash of the current commit in the package version, or store it in a configuration file. |
| 18 | +3. Derive the version from the most recent git tag rather than maintain it in the code. |
| 19 | + |
| 20 | +.. note:: |
| 21 | + |
| 22 | + Each of these things has a cost - keeping all metadata static and not |
| 23 | + running ``git`` or introspecting the ``.git`` directory as part of the build |
| 24 | + avoids running extra build steps in some cases, extra build dependencies or |
| 25 | + custom scripts, and potential issues with shallow checkouts in CI where the |
| 26 | + ``.git`` directory may not be present. Only use these dynamic features if |
| 27 | + you have a good reason to do so! |
| 28 | + |
| 29 | +Single-sourcing the version string |
| 30 | +---------------------------------- |
| 31 | + |
| 32 | +When you want to define your project's version string in a single place, |
| 33 | +``meson-python`` knows how to extract the version number from the ``project()`` |
| 34 | +call in the top-level ``meson.build``; in ``pyproject.toml`` it can be declared |
| 35 | +as dynamic: |
| 36 | + |
| 37 | +.. code-block:: toml |
| 38 | +
|
| 39 | + [project] |
| 40 | + dynamic = ['version'] |
| 41 | +
|
| 42 | +Then in ``meson.build``, define the version: |
| 43 | + |
| 44 | +.. code-block:: meson |
| 45 | +
|
| 46 | + project('my-project', 'c', version: '1.2.3') |
| 47 | +
|
| 48 | +It can also be done the other way around - this requires a bit more code, |
| 49 | +however it has the advantage that all metadata remains static in |
| 50 | +``pyproject.toml``, which can in some cases avoid triggering a build |
| 51 | +when an installer needs to obtain the version. To implement this, |
| 52 | +set the version in ``pyproject.toml``: |
| 53 | + |
| 54 | +.. code-block:: toml |
| 55 | +
|
| 56 | + [project] |
| 57 | + version = '1.2.3' |
| 58 | +
|
| 59 | +And in ``meson.build``, run a helper script as part of the project call |
| 60 | +(again, this is only needed if you actually use the version string inside a |
| 61 | +``meson.build`` file): |
| 62 | + |
| 63 | +.. code-block:: meson |
| 64 | +
|
| 65 | + project('my-project', |
| 66 | + 'c', |
| 67 | + version: run_command( |
| 68 | + ['get_version.py'], |
| 69 | + check: true |
| 70 | + ).stdout().strip(), |
| 71 | + ) |
| 72 | +
|
| 73 | +With that ``get_version.py`` script retrieving the version from |
| 74 | +``pyproject.toml``: |
| 75 | + |
| 76 | +.. code-block:: python |
| 77 | +
|
| 78 | + #!/usr/bin/env python3 |
| 79 | + import os |
| 80 | +
|
| 81 | + def get_version(): |
| 82 | + pyproject_toml = os.path.join(os.path.dirname(__file__), 'pyproject.toml') |
| 83 | + with open(pyproject_toml) as f: |
| 84 | + data = f.readlines() |
| 85 | +
|
| 86 | + version_line = next( |
| 87 | + line for line in data if line.startswith('version =') |
| 88 | + ) |
| 89 | + version = version_line.strip().split(' = ')[1] |
| 90 | + return version.replace('"', '').replace("'", '') |
| 91 | +
|
| 92 | +
|
| 93 | + if __name__ == "__main__": |
| 94 | + print(get_version()) |
| 95 | +
|
| 96 | +
|
| 97 | +Storing the git commit hash inside your package |
| 98 | +----------------------------------------------- |
| 99 | + |
| 100 | +Capturing the git commit hash alongside the version can be useful for bug |
| 101 | +reports and reproducibility: the user can print ``pkgname.__version__`` and |
| 102 | +``pkgname.__git_hash__`` to identify exactly which commit they are running. The |
| 103 | +commit hash is not part of ``pyproject.toml`` and cannot be derived from a |
| 104 | +source distribution after the fact, so it has to be written into the package at |
| 105 | +build time. |
| 106 | + |
| 107 | +A pattern to achieve this, which used by NumPy for example, is a single helper |
| 108 | +script that does double duty: it prints the version when called from |
| 109 | +``project()``, and it writes a generated ``_version.py`` file containing both |
| 110 | +the version and the git hash when called from a build step. The |
| 111 | +script is wired up via ``custom_target()`` for normal builds and via |
| 112 | +``meson.add_dist_script()`` so that the generated file is also included |
| 113 | +in source distributions. |
| 114 | + |
| 115 | +In ``meson.build``: |
| 116 | + |
| 117 | +.. literalinclude:: ../../tests/packages/dynamic-version-from-script/meson.build |
| 118 | + :language: meson |
| 119 | + :lines: 5- |
| 120 | + |
| 121 | +The ``build_always_stale: true`` flag ensures that the recorded hash is |
| 122 | +refreshed every time the project is rebuilt, rather than being cached |
| 123 | +from a previous build. |
| 124 | + |
| 125 | +The helper script reads the version from ``pyproject.toml`` (so the |
| 126 | +version still has a single source of truth) and resolves the git hash |
| 127 | +via ``git rev-parse``, falling back to ``'unknown'`` when called outside |
| 128 | +a checkout — for example when building from an extracted source |
| 129 | +distribution: |
| 130 | + |
| 131 | +.. literalinclude:: ../../tests/packages/dynamic-version-from-script/generate_version.py |
| 132 | + :language: python |
| 133 | + :lines: 1,5- |
| 134 | + |
| 135 | +The ``MESON_DIST_ROOT`` branch ensures that when the script is invoked |
| 136 | +as a dist script, it writes the generated file into the staging |
| 137 | +directory ``meson dist`` is preparing, so it is included in the source |
| 138 | +distribution. See :ref:`sdist` for the surrounding context. |
| 139 | + |
| 140 | +The package's ``__init__.py`` re-exports the generated symbols: |
| 141 | + |
| 142 | +.. code-block:: python |
| 143 | +
|
| 144 | + from pkgname._version import __git_hash__, __version__ |
| 145 | +
|
| 146 | +A complete worked example lives at ``tests/packages/dynamic-version-from-script`` |
| 147 | +in the meson-python source tree. |
| 148 | + |
| 149 | + |
| 150 | +Derive version from latest git tag |
| 151 | +---------------------------------- |
| 152 | + |
| 153 | +When the version is encoded in git tags rather than in source files, the |
| 154 | +build system has to query git at configure time. There are a number of |
| 155 | +packages that provide this functionality - all popular ones (e.g., |
| 156 | +``setuptools-scm``, ``versioneer``, ``versioningit``) can be used |
| 157 | +together with ``meson-python``. The integration principle is the same |
| 158 | +as above: use a ``run_command()`` call inside ``project()`` (either directly or |
| 159 | +through a small wrapper script like ``get_version.py`` higher up) that prints |
| 160 | +the version and (optionally) writes out a file to disk that can be included in |
| 161 | +the sdist. |
| 162 | + |
| 163 | +The example below uses ``setuptools-scm``; the same approach applies |
| 164 | +to the other tools - only the wrapper script differs. Declare it as a build |
| 165 | +requirement in ``pyproject.toml``: |
| 166 | + |
| 167 | + |
| 168 | +.. literalinclude:: ../../tests/packages/version-setuptools-scm/pyproject.toml |
| 169 | + :language: toml |
| 170 | + :lines: 5-12 |
| 171 | + |
| 172 | +In ``meson.build``, invoke ``setuptools-scm`` to compute the version: |
| 173 | + |
| 174 | +.. literalinclude:: ../../tests/packages/version-setuptools-scm/meson.build |
| 175 | + :language: meson |
| 176 | + :lines: 5- |
| 177 | + |
| 178 | +That's it. You can use ``setuptools-scm`` config options as explained in its docs. |
| 179 | +If you do want to store a generated file ``.py`` file with versioning metadata, |
| 180 | +use ``meson.add_dist_script()`` as explained higher up. |
0 commit comments