Skip to content

Commit de534d1

Browse files
committed
DOC: add a howto page on dynamic versioning
1 parent 6205047 commit de534d1

2 files changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ the use of ``meson-python`` and Meson for Python packaging.
8383
how-to-guides/meson-args
8484
how-to-guides/debug-builds
8585
how-to-guides/shared-libraries
86+
how-to-guides/dynamic-versioning
8687
reference/limitations
8788
projects-using-meson-python
8889

0 commit comments

Comments
 (0)