buildstamp 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- buildstamp-0.3.0/LICENSE +21 -0
- buildstamp-0.3.0/MANIFEST.in +1 -0
- buildstamp-0.3.0/PKG-INFO +155 -0
- buildstamp-0.3.0/README.md +124 -0
- buildstamp-0.3.0/VERSION +1 -0
- buildstamp-0.3.0/_build_backend.py +1 -0
- buildstamp-0.3.0/buildstamp/__init__.py +11 -0
- buildstamp-0.3.0/buildstamp/_metadata.py +57 -0
- buildstamp-0.3.0/buildstamp/backend.py +164 -0
- buildstamp-0.3.0/buildstamp.egg-info/PKG-INFO +155 -0
- buildstamp-0.3.0/buildstamp.egg-info/SOURCES.txt +15 -0
- buildstamp-0.3.0/buildstamp.egg-info/dependency_links.txt +1 -0
- buildstamp-0.3.0/buildstamp.egg-info/requires.txt +9 -0
- buildstamp-0.3.0/buildstamp.egg-info/top_level.txt +1 -0
- buildstamp-0.3.0/pyproject.toml +69 -0
- buildstamp-0.3.0/setup.cfg +4 -0
- buildstamp-0.3.0/tests/test_metadata.py +120 -0
buildstamp-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 buildstamp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include _build_backend.py
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: buildstamp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Build-time version metadata injection for Python packages.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/basvandriel/buildstamp
|
|
7
|
+
Project-URL: Repository, https://github.com/basvandriel/buildstamp
|
|
8
|
+
Project-URL: Issues, https://github.com/basvandriel/buildstamp/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/basvandriel/buildstamp/releases
|
|
10
|
+
Keywords: versioning,build,metadata,pep517,setuptools
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.9; extra == "dev"
|
|
28
|
+
Requires-Dist: pyright>=1.1; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# buildstamp
|
|
33
|
+
|
|
34
|
+
[](https://github.com/basvandriel/buildstamp/actions/workflows/ci.yml)
|
|
35
|
+
[](https://pypi.org/project/buildstamp/)
|
|
36
|
+
[](https://pypi.org/project/buildstamp/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
Build-time version metadata injection for Python packages. Every artifact is
|
|
40
|
+
stamped with its version, quality tier, commit SHA, and build date — with no
|
|
41
|
+
metadata drift between source and installed packages, and no git required at
|
|
42
|
+
runtime.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
| Environment | Version source |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Git checkout / editable install | Live: `git rev-parse` + `VERSION` file |
|
|
51
|
+
| Installed artifact (no `.git`) | Baked: `_version.json` written at build time |
|
|
52
|
+
|
|
53
|
+
The quality tier (`dev` / `rc` / `stable`) is controlled by the `RELEASE_TYPE`
|
|
54
|
+
environment variable at build time, so no tag ceremony is needed and no branch
|
|
55
|
+
names leak into artifacts.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
**1. Add a thin backend shim** — `_build_backend.py` at your project root:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from buildstamp.backend import * # noqa: F401, F403
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**2. Configure `pyproject.toml`:**
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[project]
|
|
71
|
+
dynamic = ["version"]
|
|
72
|
+
|
|
73
|
+
[tool.setuptools.dynamic]
|
|
74
|
+
version = {file = "VERSION"}
|
|
75
|
+
|
|
76
|
+
[tool.setuptools.package-data]
|
|
77
|
+
your_package = ["_version.json"]
|
|
78
|
+
|
|
79
|
+
[build-system]
|
|
80
|
+
requires = ["setuptools>=64.0", "wheel", "buildstamp"]
|
|
81
|
+
build-backend = "_build_backend"
|
|
82
|
+
backend-path = ["."]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**3. Add `_version.json` to `.gitignore`:**
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
your_package/_version.json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**4. Use in `your_package/__init__.py`:**
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from buildstamp import load_metadata
|
|
95
|
+
|
|
96
|
+
_meta = load_metadata(__file__)
|
|
97
|
+
__version__ = _meta.version
|
|
98
|
+
__quality__ = _meta.quality
|
|
99
|
+
__commit__ = _meta.commit
|
|
100
|
+
__build_date__ = _meta.build_date
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**5. Create a `VERSION` file** at the project root:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
1.0.0
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## `BuildMetadata` fields
|
|
112
|
+
|
|
113
|
+
| Field | Type | Dev (checkout) | Artifact |
|
|
114
|
+
|---|---|---|---|
|
|
115
|
+
| `version` | `str` | `"1.0.0+g701e4ca"` | `"1.0.0"` |
|
|
116
|
+
| `quality` | `str` | `"dev"` | `RELEASE_TYPE` value |
|
|
117
|
+
| `commit` | `str` | short SHA | short SHA baked at build time |
|
|
118
|
+
| `build_date` | `datetime \| None` | `None` | UTC datetime |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Releasing artifacts
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
# stable release
|
|
126
|
+
RELEASE_TYPE=stable uv build
|
|
127
|
+
|
|
128
|
+
# release candidate
|
|
129
|
+
RELEASE_TYPE=rc uv build
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If `RELEASE_TYPE` is unset, the baked quality defaults to `"dev"`.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Installation
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
pip install buildstamp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
While not yet on PyPI, install from source:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
uv pip install setuptools
|
|
146
|
+
uv pip install -e /path/to/buildstamp
|
|
147
|
+
uv pip install -e . --no-build-isolation
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Design rationale
|
|
153
|
+
|
|
154
|
+
See [VERSIONING.md](VERSIONING.md) for the full rationale — including why this
|
|
155
|
+
avoids git tags, dirty flags, and branch names as version sources.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# buildstamp
|
|
2
|
+
|
|
3
|
+
[](https://github.com/basvandriel/buildstamp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/buildstamp/)
|
|
5
|
+
[](https://pypi.org/project/buildstamp/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Build-time version metadata injection for Python packages. Every artifact is
|
|
9
|
+
stamped with its version, quality tier, commit SHA, and build date — with no
|
|
10
|
+
metadata drift between source and installed packages, and no git required at
|
|
11
|
+
runtime.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
| Environment | Version source |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Git checkout / editable install | Live: `git rev-parse` + `VERSION` file |
|
|
20
|
+
| Installed artifact (no `.git`) | Baked: `_version.json` written at build time |
|
|
21
|
+
|
|
22
|
+
The quality tier (`dev` / `rc` / `stable`) is controlled by the `RELEASE_TYPE`
|
|
23
|
+
environment variable at build time, so no tag ceremony is needed and no branch
|
|
24
|
+
names leak into artifacts.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
**1. Add a thin backend shim** — `_build_backend.py` at your project root:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from buildstamp.backend import * # noqa: F401, F403
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**2. Configure `pyproject.toml`:**
|
|
37
|
+
|
|
38
|
+
```toml
|
|
39
|
+
[project]
|
|
40
|
+
dynamic = ["version"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.dynamic]
|
|
43
|
+
version = {file = "VERSION"}
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
your_package = ["_version.json"]
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["setuptools>=64.0", "wheel", "buildstamp"]
|
|
50
|
+
build-backend = "_build_backend"
|
|
51
|
+
backend-path = ["."]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**3. Add `_version.json` to `.gitignore`:**
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
your_package/_version.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**4. Use in `your_package/__init__.py`:**
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from buildstamp import load_metadata
|
|
64
|
+
|
|
65
|
+
_meta = load_metadata(__file__)
|
|
66
|
+
__version__ = _meta.version
|
|
67
|
+
__quality__ = _meta.quality
|
|
68
|
+
__commit__ = _meta.commit
|
|
69
|
+
__build_date__ = _meta.build_date
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**5. Create a `VERSION` file** at the project root:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
1.0.0
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## `BuildMetadata` fields
|
|
81
|
+
|
|
82
|
+
| Field | Type | Dev (checkout) | Artifact |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| `version` | `str` | `"1.0.0+g701e4ca"` | `"1.0.0"` |
|
|
85
|
+
| `quality` | `str` | `"dev"` | `RELEASE_TYPE` value |
|
|
86
|
+
| `commit` | `str` | short SHA | short SHA baked at build time |
|
|
87
|
+
| `build_date` | `datetime \| None` | `None` | UTC datetime |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Releasing artifacts
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
# stable release
|
|
95
|
+
RELEASE_TYPE=stable uv build
|
|
96
|
+
|
|
97
|
+
# release candidate
|
|
98
|
+
RELEASE_TYPE=rc uv build
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
If `RELEASE_TYPE` is unset, the baked quality defaults to `"dev"`.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
pip install buildstamp
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
While not yet on PyPI, install from source:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
uv pip install setuptools
|
|
115
|
+
uv pip install -e /path/to/buildstamp
|
|
116
|
+
uv pip install -e . --no-build-isolation
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Design rationale
|
|
122
|
+
|
|
123
|
+
See [VERSIONING.md](VERSIONING.md) for the full rationale — including why this
|
|
124
|
+
avoids git tags, dirty flags, and branch names as version sources.
|
buildstamp-0.3.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from buildstamp.backend import * # noqa: F401, F403
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from buildstamp._metadata import BuildMetadata, load_metadata
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
_meta = load_metadata(__file__)
|
|
5
|
+
__version__ = _meta.version
|
|
6
|
+
except FileNotFoundError:
|
|
7
|
+
# No .git and no _version.json — we're being imported during a build step.
|
|
8
|
+
_meta = None
|
|
9
|
+
__version__ = "unknown"
|
|
10
|
+
|
|
11
|
+
__all__ = ["BuildMetadata", "load_metadata", "__version__"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class BuildMetadata:
|
|
12
|
+
version: str
|
|
13
|
+
quality: str
|
|
14
|
+
commit: str
|
|
15
|
+
build_date: datetime | None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _run_git(*args: str) -> str:
|
|
19
|
+
try:
|
|
20
|
+
return subprocess.check_output(["git", *args], stderr=subprocess.DEVNULL, text=True).strip()
|
|
21
|
+
except Exception:
|
|
22
|
+
return "unknown"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_metadata(package_file: str | Path) -> BuildMetadata:
|
|
26
|
+
"""Load version metadata for a package.
|
|
27
|
+
|
|
28
|
+
Call from your package's __init__.py:
|
|
29
|
+
|
|
30
|
+
from buildstamp import load_metadata
|
|
31
|
+
|
|
32
|
+
_meta = load_metadata(__file__)
|
|
33
|
+
__version__ = _meta.version
|
|
34
|
+
__quality__ = _meta.quality
|
|
35
|
+
__commit__ = _meta.commit
|
|
36
|
+
__build_date__ = _meta.build_date
|
|
37
|
+
|
|
38
|
+
In a git checkout (development or editable install), metadata is computed
|
|
39
|
+
live from git — always accurate, never stale. In an installed artifact
|
|
40
|
+
(no .git), it is read from _version.json baked in at build time.
|
|
41
|
+
"""
|
|
42
|
+
package_dir = Path(package_file).parent
|
|
43
|
+
repo_root = package_dir.parent
|
|
44
|
+
|
|
45
|
+
if (repo_root / ".git").exists():
|
|
46
|
+
base = (repo_root / "VERSION").read_text(encoding="utf-8").strip()
|
|
47
|
+
sha = _run_git("rev-parse", "--short", "HEAD")
|
|
48
|
+
version = f"{base}+g{sha}" if sha != "unknown" else f"{base}.dev0"
|
|
49
|
+
return BuildMetadata(version=version, quality="dev", commit=sha, build_date=None)
|
|
50
|
+
|
|
51
|
+
meta = json.loads((package_dir / "_version.json").read_text(encoding="utf-8"))
|
|
52
|
+
return BuildMetadata(
|
|
53
|
+
version=meta["version"],
|
|
54
|
+
quality=meta["quality"],
|
|
55
|
+
commit=meta["commit"],
|
|
56
|
+
build_date=datetime.fromisoformat(meta["build_date"]),
|
|
57
|
+
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""PEP 517 build backend that injects VCS metadata at build time.
|
|
2
|
+
|
|
3
|
+
Import all hooks from this module in your project's thin _build_backend.py:
|
|
4
|
+
|
|
5
|
+
from buildstamp.backend import * # noqa: F401, F403
|
|
6
|
+
|
|
7
|
+
Then in pyproject.toml:
|
|
8
|
+
|
|
9
|
+
[build-system]
|
|
10
|
+
requires = ["setuptools>=64.0", "wheel", "buildstamp"]
|
|
11
|
+
build-backend = "_build_backend"
|
|
12
|
+
backend-path = ["."]
|
|
13
|
+
|
|
14
|
+
buildstamp reads [tool.buildstamp] from pyproject.toml for configuration.
|
|
15
|
+
If not present, the package name is derived from [project].name.
|
|
16
|
+
|
|
17
|
+
[tool.buildstamp]
|
|
18
|
+
metadata-file = "your_package/_version.json" # optional override
|
|
19
|
+
version-file = "VERSION" # optional override (default: VERSION)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import subprocess
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from setuptools.build_meta import (
|
|
31
|
+
build_sdist as _build_sdist,
|
|
32
|
+
)
|
|
33
|
+
from setuptools.build_meta import (
|
|
34
|
+
build_wheel as _build_wheel,
|
|
35
|
+
)
|
|
36
|
+
from setuptools.build_meta import (
|
|
37
|
+
get_requires_for_build_editable,
|
|
38
|
+
get_requires_for_build_sdist,
|
|
39
|
+
get_requires_for_build_wheel,
|
|
40
|
+
)
|
|
41
|
+
from setuptools.build_meta import (
|
|
42
|
+
prepare_metadata_for_build_wheel as _prepare_wheel,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"build_editable",
|
|
47
|
+
"build_sdist",
|
|
48
|
+
"build_wheel",
|
|
49
|
+
"get_requires_for_build_editable",
|
|
50
|
+
"get_requires_for_build_sdist",
|
|
51
|
+
"get_requires_for_build_wheel",
|
|
52
|
+
"prepare_metadata_for_build_editable",
|
|
53
|
+
"prepare_metadata_for_build_wheel",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _git(*args: str) -> str:
|
|
58
|
+
try:
|
|
59
|
+
return subprocess.check_output(["git", *args], stderr=subprocess.DEVNULL, text=True).strip()
|
|
60
|
+
except Exception:
|
|
61
|
+
return "unknown"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _read_config() -> tuple[Path, Path]:
|
|
65
|
+
"""Return (metadata_file, version_file) from pyproject.toml config."""
|
|
66
|
+
try:
|
|
67
|
+
try:
|
|
68
|
+
import tomllib
|
|
69
|
+
except ImportError:
|
|
70
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
71
|
+
|
|
72
|
+
with open("pyproject.toml", "rb") as f:
|
|
73
|
+
config = tomllib.load(f)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise RuntimeError(f"buildstamp: could not read pyproject.toml — {e}") from e
|
|
76
|
+
|
|
77
|
+
bs = config.get("tool", {}).get("buildstamp", {})
|
|
78
|
+
version_file = Path(bs.get("version-file", "VERSION"))
|
|
79
|
+
|
|
80
|
+
if "metadata-file" in bs:
|
|
81
|
+
return Path(bs["metadata-file"]), version_file
|
|
82
|
+
|
|
83
|
+
# Derive from [project].name: rsync-server → rsync_server/_version.json
|
|
84
|
+
try:
|
|
85
|
+
name = config["project"]["name"].replace("-", "_")
|
|
86
|
+
return Path(name) / "_version.json", version_file
|
|
87
|
+
except KeyError as exc:
|
|
88
|
+
raise RuntimeError(
|
|
89
|
+
"buildstamp: could not determine metadata file path. "
|
|
90
|
+
"Add [tool.buildstamp] metadata-file = 'your_package/_version.json' "
|
|
91
|
+
"to pyproject.toml."
|
|
92
|
+
) from exc
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def write_version_file() -> None:
|
|
96
|
+
"""Write _version.json with build-time metadata.
|
|
97
|
+
|
|
98
|
+
Called automatically by all PEP 517 hooks. Can also be called manually.
|
|
99
|
+
"""
|
|
100
|
+
metadata_file, version_file = _read_config()
|
|
101
|
+
commit = _git("rev-parse", "--short", "HEAD")
|
|
102
|
+
|
|
103
|
+
# sdist → wheel: no .git dir, reuse the JSON baked in during the sdist step.
|
|
104
|
+
if commit == "unknown" and metadata_file.exists():
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
base = version_file.read_text(encoding="utf-8").strip()
|
|
108
|
+
raw = os.environ.get("RELEASE_TYPE", "dev").strip().lower()
|
|
109
|
+
if raw not in ("dev", "rc", "stable"):
|
|
110
|
+
raise ValueError(f"RELEASE_TYPE must be 'dev', 'rc', or 'stable' — got: {raw!r}")
|
|
111
|
+
if raw == "stable":
|
|
112
|
+
version = base
|
|
113
|
+
elif raw == "rc":
|
|
114
|
+
version = f"{base}rc1"
|
|
115
|
+
else:
|
|
116
|
+
version = f"{base}.dev0"
|
|
117
|
+
|
|
118
|
+
metadata_file.write_text(
|
|
119
|
+
json.dumps(
|
|
120
|
+
{
|
|
121
|
+
"version": version,
|
|
122
|
+
"quality": raw,
|
|
123
|
+
"commit": commit,
|
|
124
|
+
"build_date": datetime.now(timezone.utc).isoformat(),
|
|
125
|
+
},
|
|
126
|
+
indent=2,
|
|
127
|
+
)
|
|
128
|
+
+ "\n",
|
|
129
|
+
encoding="utf-8",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# --- PEP 517 hooks -----------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
|
|
137
|
+
write_version_file()
|
|
138
|
+
return _prepare_wheel(metadata_directory, config_settings)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
|
142
|
+
write_version_file()
|
|
143
|
+
return _build_wheel(wheel_directory, config_settings, metadata_directory)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def build_sdist(sdist_directory, config_settings=None):
|
|
147
|
+
write_version_file()
|
|
148
|
+
return _build_sdist(sdist_directory, config_settings)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def prepare_metadata_for_build_editable(metadata_directory, config_settings=None):
|
|
152
|
+
from setuptools.build_meta import (
|
|
153
|
+
prepare_metadata_for_build_editable as _prepare_editable,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
write_version_file()
|
|
157
|
+
return _prepare_editable(metadata_directory, config_settings)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
|
|
161
|
+
from setuptools.build_meta import build_editable as _build_editable
|
|
162
|
+
|
|
163
|
+
write_version_file()
|
|
164
|
+
return _build_editable(wheel_directory, config_settings, metadata_directory)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: buildstamp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Build-time version metadata injection for Python packages.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/basvandriel/buildstamp
|
|
7
|
+
Project-URL: Repository, https://github.com/basvandriel/buildstamp
|
|
8
|
+
Project-URL: Issues, https://github.com/basvandriel/buildstamp/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/basvandriel/buildstamp/releases
|
|
10
|
+
Keywords: versioning,build,metadata,pep517,setuptools
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.9; extra == "dev"
|
|
28
|
+
Requires-Dist: pyright>=1.1; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# buildstamp
|
|
33
|
+
|
|
34
|
+
[](https://github.com/basvandriel/buildstamp/actions/workflows/ci.yml)
|
|
35
|
+
[](https://pypi.org/project/buildstamp/)
|
|
36
|
+
[](https://pypi.org/project/buildstamp/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
Build-time version metadata injection for Python packages. Every artifact is
|
|
40
|
+
stamped with its version, quality tier, commit SHA, and build date — with no
|
|
41
|
+
metadata drift between source and installed packages, and no git required at
|
|
42
|
+
runtime.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
| Environment | Version source |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Git checkout / editable install | Live: `git rev-parse` + `VERSION` file |
|
|
51
|
+
| Installed artifact (no `.git`) | Baked: `_version.json` written at build time |
|
|
52
|
+
|
|
53
|
+
The quality tier (`dev` / `rc` / `stable`) is controlled by the `RELEASE_TYPE`
|
|
54
|
+
environment variable at build time, so no tag ceremony is needed and no branch
|
|
55
|
+
names leak into artifacts.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
**1. Add a thin backend shim** — `_build_backend.py` at your project root:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from buildstamp.backend import * # noqa: F401, F403
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**2. Configure `pyproject.toml`:**
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[project]
|
|
71
|
+
dynamic = ["version"]
|
|
72
|
+
|
|
73
|
+
[tool.setuptools.dynamic]
|
|
74
|
+
version = {file = "VERSION"}
|
|
75
|
+
|
|
76
|
+
[tool.setuptools.package-data]
|
|
77
|
+
your_package = ["_version.json"]
|
|
78
|
+
|
|
79
|
+
[build-system]
|
|
80
|
+
requires = ["setuptools>=64.0", "wheel", "buildstamp"]
|
|
81
|
+
build-backend = "_build_backend"
|
|
82
|
+
backend-path = ["."]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**3. Add `_version.json` to `.gitignore`:**
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
your_package/_version.json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**4. Use in `your_package/__init__.py`:**
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from buildstamp import load_metadata
|
|
95
|
+
|
|
96
|
+
_meta = load_metadata(__file__)
|
|
97
|
+
__version__ = _meta.version
|
|
98
|
+
__quality__ = _meta.quality
|
|
99
|
+
__commit__ = _meta.commit
|
|
100
|
+
__build_date__ = _meta.build_date
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**5. Create a `VERSION` file** at the project root:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
1.0.0
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## `BuildMetadata` fields
|
|
112
|
+
|
|
113
|
+
| Field | Type | Dev (checkout) | Artifact |
|
|
114
|
+
|---|---|---|---|
|
|
115
|
+
| `version` | `str` | `"1.0.0+g701e4ca"` | `"1.0.0"` |
|
|
116
|
+
| `quality` | `str` | `"dev"` | `RELEASE_TYPE` value |
|
|
117
|
+
| `commit` | `str` | short SHA | short SHA baked at build time |
|
|
118
|
+
| `build_date` | `datetime \| None` | `None` | UTC datetime |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Releasing artifacts
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
# stable release
|
|
126
|
+
RELEASE_TYPE=stable uv build
|
|
127
|
+
|
|
128
|
+
# release candidate
|
|
129
|
+
RELEASE_TYPE=rc uv build
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If `RELEASE_TYPE` is unset, the baked quality defaults to `"dev"`.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Installation
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
pip install buildstamp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
While not yet on PyPI, install from source:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
uv pip install setuptools
|
|
146
|
+
uv pip install -e /path/to/buildstamp
|
|
147
|
+
uv pip install -e . --no-build-isolation
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Design rationale
|
|
153
|
+
|
|
154
|
+
See [VERSIONING.md](VERSIONING.md) for the full rationale — including why this
|
|
155
|
+
avoids git tags, dirty flags, and branch names as version sources.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
VERSION
|
|
5
|
+
_build_backend.py
|
|
6
|
+
pyproject.toml
|
|
7
|
+
buildstamp/__init__.py
|
|
8
|
+
buildstamp/_metadata.py
|
|
9
|
+
buildstamp/backend.py
|
|
10
|
+
buildstamp.egg-info/PKG-INFO
|
|
11
|
+
buildstamp.egg-info/SOURCES.txt
|
|
12
|
+
buildstamp.egg-info/dependency_links.txt
|
|
13
|
+
buildstamp.egg-info/requires.txt
|
|
14
|
+
buildstamp.egg-info/top_level.txt
|
|
15
|
+
tests/test_metadata.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
buildstamp
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "buildstamp"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Build-time version metadata injection for Python packages."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
keywords = ["versioning", "build", "metadata", "pep517", "setuptools"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.10",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: Software Development :: Build Tools",
|
|
18
|
+
"Topic :: Software Development :: Version Control",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=8",
|
|
28
|
+
"ruff>=0.9",
|
|
29
|
+
"pyright>=1.1",
|
|
30
|
+
"twine>=5",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/basvandriel/buildstamp"
|
|
35
|
+
Repository = "https://github.com/basvandriel/buildstamp"
|
|
36
|
+
Issues = "https://github.com/basvandriel/buildstamp/issues"
|
|
37
|
+
Changelog = "https://github.com/basvandriel/buildstamp/releases"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["setuptools>=64.0"]
|
|
41
|
+
build-backend = "_build_backend"
|
|
42
|
+
backend-path = ["."]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["."]
|
|
46
|
+
include = ["buildstamp*"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.dynamic]
|
|
49
|
+
version = {file = "VERSION"}
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
56
|
+
ignore = ["E501"]
|
|
57
|
+
|
|
58
|
+
[tool.pyright]
|
|
59
|
+
pythonVersion = "3.10"
|
|
60
|
+
strict = true
|
|
61
|
+
include = ["buildstamp"]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
|
|
66
|
+
[dependency-groups]
|
|
67
|
+
dev = [
|
|
68
|
+
"setuptools>=82.0.1",
|
|
69
|
+
]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Tests for buildstamp._metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from buildstamp._metadata import BuildMetadata, load_metadata
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# BuildMetadata
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_build_metadata_is_frozen() -> None:
|
|
20
|
+
meta = BuildMetadata(version="1.0.0", quality="dev", commit="abc1234", build_date=None)
|
|
21
|
+
with pytest.raises((AttributeError, TypeError)):
|
|
22
|
+
meta.version = "2.0.0" # type: ignore[misc]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_build_metadata_slots() -> None:
|
|
26
|
+
meta = BuildMetadata(version="1.0.0", quality="dev", commit="abc1234", build_date=None)
|
|
27
|
+
assert not hasattr(meta, "__dict__")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# load_metadata — git checkout path
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _make_pkg(root: Path, version: str = "1.2.3") -> Path:
|
|
36
|
+
"""Create a minimal package tree under root and return the __init__.py path."""
|
|
37
|
+
pkg = root / "mypkg"
|
|
38
|
+
pkg.mkdir()
|
|
39
|
+
(pkg / "__init__.py").write_text("")
|
|
40
|
+
(root / "VERSION").write_text(f"{version}\n")
|
|
41
|
+
return pkg / "__init__.py"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_load_metadata_git_checkout(tmp_path: Path) -> None:
|
|
45
|
+
(tmp_path / ".git").mkdir()
|
|
46
|
+
init = _make_pkg(tmp_path)
|
|
47
|
+
|
|
48
|
+
with patch("buildstamp._metadata._run_git", return_value="abc1234"):
|
|
49
|
+
meta = load_metadata(init)
|
|
50
|
+
|
|
51
|
+
assert meta.version == "1.2.3+gabc1234"
|
|
52
|
+
assert meta.quality == "dev"
|
|
53
|
+
assert meta.commit == "abc1234"
|
|
54
|
+
assert meta.build_date is None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_load_metadata_git_unknown_sha(tmp_path: Path) -> None:
|
|
58
|
+
"""When git is not available, version should fall back to .dev0."""
|
|
59
|
+
(tmp_path / ".git").mkdir()
|
|
60
|
+
init = _make_pkg(tmp_path)
|
|
61
|
+
|
|
62
|
+
with patch("buildstamp._metadata._run_git", return_value="unknown"):
|
|
63
|
+
meta = load_metadata(init)
|
|
64
|
+
|
|
65
|
+
assert meta.version == "1.2.3.dev0"
|
|
66
|
+
assert meta.commit == "unknown"
|
|
67
|
+
assert meta.build_date is None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_load_metadata_git_uses_version_file(tmp_path: Path) -> None:
|
|
71
|
+
(tmp_path / ".git").mkdir()
|
|
72
|
+
init = _make_pkg(tmp_path, version="2.0.0")
|
|
73
|
+
|
|
74
|
+
with patch("buildstamp._metadata._run_git", return_value="what"):
|
|
75
|
+
meta = load_metadata(init)
|
|
76
|
+
|
|
77
|
+
assert meta.version.startswith("2.0.0+g")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# load_metadata — artifact / installed package path
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_load_metadata_from_version_json(tmp_path: Path) -> None:
|
|
86
|
+
"""Without .git, metadata is read from _version.json."""
|
|
87
|
+
pkg = tmp_path / "mypkg"
|
|
88
|
+
pkg.mkdir()
|
|
89
|
+
init = pkg / "__init__.py"
|
|
90
|
+
init.write_text("")
|
|
91
|
+
|
|
92
|
+
build_date = "2026-04-10T12:00:00+00:00"
|
|
93
|
+
(pkg / "_version.json").write_text(
|
|
94
|
+
json.dumps(
|
|
95
|
+
{
|
|
96
|
+
"version": "1.2.3",
|
|
97
|
+
"quality": "stable",
|
|
98
|
+
"commit": "abc1234",
|
|
99
|
+
"build_date": build_date,
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
meta = load_metadata(init)
|
|
105
|
+
|
|
106
|
+
assert meta.version == "1.2.3"
|
|
107
|
+
assert meta.quality == "stable"
|
|
108
|
+
assert meta.commit == "abc1234"
|
|
109
|
+
assert meta.build_date == datetime.fromisoformat(build_date)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_load_metadata_missing_json_raises(tmp_path: Path) -> None:
|
|
113
|
+
pkg = tmp_path / "mypkg"
|
|
114
|
+
pkg.mkdir()
|
|
115
|
+
init = pkg / "__init__.py"
|
|
116
|
+
init.write_text("")
|
|
117
|
+
# no _version.json, no .git
|
|
118
|
+
|
|
119
|
+
with pytest.raises((FileNotFoundError, OSError)):
|
|
120
|
+
load_metadata(init)
|