Skip to content

Commit 9dd7c2a

Browse files
feat(packagesettings): add ${__version__} template variable to env settings
Inject the resolved package version into template_env as __version__, so package env settings can reference it via ${__version__}. Closes: #946 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent 8d850ff commit 9dd7c2a

11 files changed

Lines changed: 271 additions & 7 deletions

File tree

docs/customization.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,86 @@ implemented. A literal `$` must be quoted as `$$`.
215215
Added support for default value syntax `${NAME:-}`.
216216
```
217217

218+
```{versionchanged} 0.76.0
219+
220+
Added the `${__version__}` template variable.
221+
```
222+
223+
#### The `__version__` variable
224+
225+
When fromager resolves the version of a package it injects the version
226+
string into the template environment as `__version__`. You can
227+
reference it in env values with `${__version__}`.
228+
229+
##### Fixing setuptools-scm / flit_scm builds
230+
231+
Packages that use `setuptools-scm` or `flit_scm` for version detection fail
232+
when built from source archives without `.git` metadata. The build backend
233+
raises a `LookupError` during import because it cannot determine the version.
234+
235+
Setting `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_{DIST_NAME}` tells
236+
setuptools-scm to use the provided version instead of reading from SCM.
237+
This is checked before any `.git` or `PKG-INFO` lookup, so the error is
238+
avoided entirely.
239+
240+
Use the **per-package** form with the distribution name normalized to
241+
uppercase with hyphens, dots, and underscores replaced by a single `_`
242+
(adapted [PEP 503](https://peps.python.org/pep-0503/) semantics). For
243+
example, for a package named `foo`:
244+
245+
```yaml
246+
env:
247+
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_FOO: "${__version__}"
248+
```
249+
250+
The per-package form is
251+
[preferred by setuptools-scm](https://setuptools-scm.readthedocs.io/en/latest/config/)
252+
over the generic `SETUPTOOLS_SCM_PRETEND_VERSION`, which can leak into
253+
other setuptools-scm packages in the same build environment.
254+
255+
This is simpler than writing a custom `prepare_source` plugin override
256+
to inject version metadata.
257+
258+
##### Availability
259+
260+
`__version__` is set for `build_sdist`, `build_wheel`, and all
261+
dependency hooks (`get_build_backend_dependencies`,
262+
`get_build_sdist_dependencies`, etc.) that run *after* version
263+
resolution.
264+
265+
It is **not** available:
266+
267+
- During the `resolve` phase itself — the version has not yet been
268+
determined.
269+
- When bootstrapping from a **git URL whose reference is not a valid
270+
PEP 440 version** (for example
271+
`pkg @ git+https://host/repo.git` or
272+
`pkg @ git+https://host/repo.git@main`). In this case fromager
273+
must build the package metadata just to discover the version, so
274+
the early dependency-resolution hooks run with `version=None`.
275+
276+
If your env var is used in a phase where the version might be
277+
unknown, add a fallback default so the substitution does not fail:
278+
279+
```yaml
280+
env:
281+
# safe — falls back to empty string when version is not yet known
282+
MY_VAR: "${__version__:-}"
283+
```
284+
285+
Without the fallback, a bare `${__version__}` raises an error when
286+
the version is unavailable.
287+
288+
##### Examples
289+
218290
```yaml
219-
# example
220291
env:
292+
# fix setuptools-scm builds from source archives (use per-package form)
293+
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_FOO: "${__version__}"
294+
# use the resolved package version in a download URL
295+
LIB_URL: "https://github.com/org/lib/archive/v${__version__}.tar.gz"
296+
# safe for git-URL bootstrapping where version may not yet be known
297+
OPTIONAL_URL: "https://example.com/lib-${__version__:-latest}.tar.gz"
221298
# pre-pend '/global/bin' to PATH
222299
PATH: "/global/bin:$PATH"
223300
# default CFLAGS to empty string and append " -g"

e2e/ci_bootstrap_suite.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ run_test "bootstrap_sdist_only"
2929
test_section "bootstrap git URL tests"
3030
run_test "bootstrap_git_url"
3131
run_test "bootstrap_git_url_tag"
32+
run_test "version_env_git_url"
3233

3334
finish_suite

e2e/stevedore_override/src/package_plugins/stevedore.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ def update_extra_environ(
2323
marker = ctx.work_dir / "update_extra_environ.txt"
2424
with marker.open(encoding="utf-8", mode="a") as f:
2525
f.write(f"{version}\n")
26+
version_var = extra_environ.get("TEST_VERSION_VAR")
27+
if version_var is not None:
28+
version_var_marker = ctx.work_dir / "test_version_var.txt"
29+
with version_var_marker.open(encoding="utf-8", mode="w") as f:
30+
f.write(f"{version_var}\n")
2631
return None
2732

2833

e2e/test_pep517_build_sdist.sh

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,20 @@ fromager \
3838
--wheels-repo "$OUTDIR/wheels-repo" \
3939
step prepare-build --wheel-server-url "https://pypi.org/simple/" "$DIST" "$VERSION"
4040

41-
# Build an updated sdist
41+
# Build an updated sdist (with settings that exercise ${__version__} in env)
4242
rm -rf "$OUTDIR/sdists-repo/builds"
4343
fromager \
4444
--log-file "$OUTDIR/build-logs/${DIST}-build-sdist.log" \
4545
--work-dir "$OUTDIR/work-dir" \
4646
--sdists-repo "$OUTDIR/sdists-repo" \
4747
--wheels-repo "$OUTDIR/wheels-repo" \
48+
--settings-dir="$SCRIPTDIR/version_env_settings" \
4849
step build-sdist "$DIST" "$VERSION"
4950

5051
EXPECTED_FILES="
5152
$OUTDIR/sdists-repo/builds/stevedore-*.tar.gz
5253
$OUTDIR/work-dir/update_extra_environ.txt
54+
$OUTDIR/work-dir/test_version_var.txt
5355
"
5456

5557
pass=true
@@ -60,6 +62,13 @@ for pattern in $EXPECTED_FILES; do
6062
fi
6163
done
6264

65+
# Verify ${__version__} was correctly substituted in the env var
66+
actual_version=$(cat "$OUTDIR/work-dir/test_version_var.txt" | tr -d '[:space:]')
67+
if [ "$actual_version" != "$VERSION" ]; then
68+
echo "FAIL: TEST_VERSION_VAR='$actual_version', expected '$VERSION'" 1>&2
69+
pass=false
70+
fi
71+
6372
$pass
6473

6574
twine check $OUTDIR/sdists-repo/builds/*.tar.gz

e2e/test_version_env_git_url.sh

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/bin/bash
2+
# -*- indent-tabs-mode: nil; tab-width: 2; sh-indentation: 2; -*-
3+
4+
# Test that ${__version__} in env settings fails when bootstrapping from
5+
# a git URL without a PEP 440 version tag, and succeeds when a fallback
6+
# default is provided via ${__version__:-...}.
7+
8+
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
9+
source "$SCRIPTDIR/common.sh"
10+
11+
GIT_REPO_URL="https://github.com/python-wheel-build/stevedore-test-repo.git"
12+
13+
pass=true
14+
15+
# --- Part 1: ${__version__} WITHOUT default should fail ---
16+
17+
echo "=== Part 1: expect failure with \${__version__} (no default) ==="
18+
19+
if fromager \
20+
--log-file="$OUTDIR/bootstrap-no-default.log" \
21+
--error-log-file="$OUTDIR/fromager-errors-no-default.log" \
22+
--sdists-repo="$OUTDIR/sdists-repo" \
23+
--wheels-repo="$OUTDIR/wheels-repo" \
24+
--work-dir="$OUTDIR/work-dir" \
25+
--settings-dir="$SCRIPTDIR/version_env_settings_no_default" \
26+
bootstrap "stevedore @ git+${GIT_REPO_URL}" 2>&1; then
27+
echo "FAIL: bootstrap with \${__version__} (no default) should have failed" 1>&2
28+
pass=false
29+
else
30+
echo "OK: bootstrap with \${__version__} (no default) failed as expected"
31+
if grep -q "__version__" "$OUTDIR/fromager-errors-no-default.log" 2>/dev/null || \
32+
grep -q "__version__" "$OUTDIR/bootstrap-no-default.log" 2>/dev/null; then
33+
echo "OK: error message mentions __version__"
34+
else
35+
echo "WARN: error log does not mention __version__; check logs manually"
36+
fi
37+
fi
38+
39+
# --- Part 2: ${__version__:-unresolved} WITH default should succeed ---
40+
41+
echo "=== Part 2: expect success with \${__version__:-unresolved} ==="
42+
43+
rm -rf "$OUTDIR/work-dir" "$OUTDIR/sdists-repo" "$OUTDIR/wheels-repo"
44+
mkdir -p "$OUTDIR/build-logs"
45+
46+
fromager \
47+
--log-file="$OUTDIR/bootstrap-with-default.log" \
48+
--error-log-file="$OUTDIR/fromager-errors-with-default.log" \
49+
--sdists-repo="$OUTDIR/sdists-repo" \
50+
--wheels-repo="$OUTDIR/wheels-repo" \
51+
--work-dir="$OUTDIR/work-dir" \
52+
--settings-dir="$SCRIPTDIR/version_env_settings_with_default" \
53+
bootstrap "stevedore @ git+${GIT_REPO_URL}"
54+
55+
EXPECTED_FILES="
56+
$OUTDIR/wheels-repo/downloads/stevedore-*.whl
57+
$OUTDIR/sdists-repo/builds/stevedore-*.tar.gz
58+
"
59+
60+
for pattern in $EXPECTED_FILES; do
61+
if [ ! -f "${pattern}" ]; then
62+
echo "FAIL: Did not find $pattern" 1>&2
63+
pass=false
64+
fi
65+
done
66+
67+
$pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
env:
2+
TEST_VERSION_VAR: "${__version__}"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
env:
2+
TEST_VERSION_VAR: "${__version__}"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
env:
2+
TEST_VERSION_VAR: "${__version__:-unresolved}"

src/fromager/packagesettings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,13 @@ def get_extra_environ(
899899
else:
900900
template_env = template_env.copy()
901901

902+
if version is not None:
903+
template_env["__version__"] = str(version)
904+
else:
905+
# Prevent a stray __version__ in os.environ from being
906+
# silently used when the real version is unknown.
907+
template_env.pop("__version__", None)
908+
902909
# configure max jobs settings, settings depend on package, available
903910
# CPU cores, and available virtual memory.
904911
jobs = self.parallel_jobs()

0 commit comments

Comments
 (0)