Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,58 @@ implemented. A literal `$` must be quoted as `$$`.
Added support for default value syntax `${NAME:-}`.
```

```{versionchanged} 0.76.0

Added the `${__version__}` template variable.
```

#### The `__version__` variable

When fromager resolves the version of a package it injects the version
string into the template environment as `__version__`. You can
reference it in env values with `${__version__}`.

**When it is available:**

`__version__` is set for `build_sdist`, `build_wheel`, and all
dependency hooks (`get_build_backend_dependencies`,
`get_build_sdist_dependencies`, etc.) that run *after* version
resolution.

**When it is NOT available:**

- During the `resolve` phase itself — the version has not yet been
determined.
- When bootstrapping from a **git URL whose reference is not a valid
PEP 440 version** (for example
`pkg @ git+https://host/repo.git` or
`pkg @ git+https://host/repo.git@main`). In this case fromager
must build the package metadata just to discover the version, so
the early dependency-resolution hooks run with `version=None`.

If your env var is used in a phase where the version might be
unknown, add a fallback default so the substitution does not fail:

```yaml
env:
# safe — falls back to empty string when version is not yet known
MY_VAR: "${__version__:-}"
```

Without the fallback, a bare `${__version__}` raises an error when
the version is unavailable.

```yaml
# example
env:
# pre-pend '/global/bin' to PATH
PATH: "/global/bin:$PATH"
# default CFLAGS to empty string and append " -g"
CFLAGS: "${CFLAGS:-} -g"
# use the resolved package version in a build env variable
LIB_URL: "https://github.com/org/lib/archive/v${__version__}.tar.gz"
# safe for git-URL bootstrapping where version may not yet be known
OPTIONAL_URL: "https://example.com/lib-${__version__:-latest}.tar.gz"
variants:
cpu:
env:
Expand Down
1 change: 1 addition & 0 deletions e2e/ci_bootstrap_suite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ run_test "bootstrap_sdist_only"
test_section "bootstrap git URL tests"
run_test "bootstrap_git_url"
run_test "bootstrap_git_url_tag"
run_test "version_env_git_url"

finish_suite
5 changes: 5 additions & 0 deletions e2e/stevedore_override/src/package_plugins/stevedore.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ def update_extra_environ(
marker = ctx.work_dir / "update_extra_environ.txt"
with marker.open(encoding="utf-8", mode="a") as f:
f.write(f"{version}\n")
version_var = extra_environ.get("TEST_VERSION_VAR")
if version_var is not None:
version_var_marker = ctx.work_dir / "test_version_var.txt"
with version_var_marker.open(encoding="utf-8", mode="w") as f:
f.write(f"{version_var}\n")
return None


Expand Down
11 changes: 10 additions & 1 deletion e2e/test_pep517_build_sdist.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,20 @@ fromager \
--wheels-repo "$OUTDIR/wheels-repo" \
step prepare-build --wheel-server-url "https://pypi.org/simple/" "$DIST" "$VERSION"

# Build an updated sdist
# Build an updated sdist (with settings that exercise ${__version__} in env)
rm -rf "$OUTDIR/sdists-repo/builds"
fromager \
--log-file "$OUTDIR/build-logs/${DIST}-build-sdist.log" \
--work-dir "$OUTDIR/work-dir" \
--sdists-repo "$OUTDIR/sdists-repo" \
--wheels-repo "$OUTDIR/wheels-repo" \
--settings-dir="$SCRIPTDIR/version_env_settings" \
step build-sdist "$DIST" "$VERSION"

EXPECTED_FILES="
$OUTDIR/sdists-repo/builds/stevedore-*.tar.gz
$OUTDIR/work-dir/update_extra_environ.txt
$OUTDIR/work-dir/test_version_var.txt
"

pass=true
Expand All @@ -60,6 +62,13 @@ for pattern in $EXPECTED_FILES; do
fi
done

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

$pass

twine check $OUTDIR/sdists-repo/builds/*.tar.gz
67 changes: 67 additions & 0 deletions e2e/test_version_env_git_url.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/bin/bash
# -*- indent-tabs-mode: nil; tab-width: 2; sh-indentation: 2; -*-

# Test that ${__version__} in env settings fails when bootstrapping from
# a git URL without a PEP 440 version tag, and succeeds when a fallback
# default is provided via ${__version__:-...}.

SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source "$SCRIPTDIR/common.sh"

GIT_REPO_URL="https://github.com/python-wheel-build/stevedore-test-repo.git"

pass=true

# --- Part 1: ${__version__} WITHOUT default should fail ---

echo "=== Part 1: expect failure with \${__version__} (no default) ==="

if fromager \
--log-file="$OUTDIR/bootstrap-no-default.log" \
--error-log-file="$OUTDIR/fromager-errors-no-default.log" \
--sdists-repo="$OUTDIR/sdists-repo" \
--wheels-repo="$OUTDIR/wheels-repo" \
--work-dir="$OUTDIR/work-dir" \
--settings-dir="$SCRIPTDIR/version_env_settings_no_default" \
bootstrap "stevedore @ git+${GIT_REPO_URL}" 2>&1; then
echo "FAIL: bootstrap with \${__version__} (no default) should have failed" 1>&2
pass=false
else
echo "OK: bootstrap with \${__version__} (no default) failed as expected"
if grep -q "__version__" "$OUTDIR/fromager-errors-no-default.log" 2>/dev/null || \
grep -q "__version__" "$OUTDIR/bootstrap-no-default.log" 2>/dev/null; then
echo "OK: error message mentions __version__"
else
echo "WARN: error log does not mention __version__; check logs manually"
fi
fi

# --- Part 2: ${__version__:-unresolved} WITH default should succeed ---

echo "=== Part 2: expect success with \${__version__:-unresolved} ==="

rm -rf "$OUTDIR/work-dir" "$OUTDIR/sdists-repo" "$OUTDIR/wheels-repo"
mkdir -p "$OUTDIR/build-logs"

fromager \
--log-file="$OUTDIR/bootstrap-with-default.log" \
--error-log-file="$OUTDIR/fromager-errors-with-default.log" \
--sdists-repo="$OUTDIR/sdists-repo" \
--wheels-repo="$OUTDIR/wheels-repo" \
--work-dir="$OUTDIR/work-dir" \
--settings-dir="$SCRIPTDIR/version_env_settings_with_default" \
bootstrap "stevedore @ git+${GIT_REPO_URL}"

EXPECTED_FILES="
$OUTDIR/wheels-repo/downloads/stevedore-*.whl
$OUTDIR/sdists-repo/builds/stevedore-*.tar.gz
"

for pattern in $EXPECTED_FILES; do
if [ ! -f "${pattern}" ]; then
echo "FAIL: Did not find $pattern" 1>&2
pass=false
fi
done

$pass
2 changes: 2 additions & 0 deletions e2e/version_env_settings/stevedore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
env:
TEST_VERSION_VAR: "${__version__}"
2 changes: 2 additions & 0 deletions e2e/version_env_settings_no_default/stevedore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
env:
TEST_VERSION_VAR: "${__version__}"
2 changes: 2 additions & 0 deletions e2e/version_env_settings_with_default/stevedore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
env:
TEST_VERSION_VAR: "${__version__:-unresolved}"
7 changes: 7 additions & 0 deletions src/fromager/packagesettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,13 @@ def get_extra_environ(
else:
template_env = template_env.copy()

if version is not None:
template_env["__version__"] = str(version)
else:
# Prevent a stray __version__ in os.environ from being
# silently used when the real version is unknown.
template_env.pop("__version__", None)

# configure max jobs settings, settings depend on package, available
# CPU cores, and available virtual memory.
jobs = self.parallel_jobs()
Expand Down
101 changes: 96 additions & 5 deletions tests/test_packagesettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
EnvVars,
GitOptions,
Package,
PackageBuildInfo,
PackageSettings,
ResolverDist,
Settings,
SettingsFile,
Variant,
substitute_template,
Expand Down Expand Up @@ -62,6 +64,7 @@
"QUOTES": "A\"BC'$$EGG",
"DEF": "${DEF:-default}",
"EXTRA_MAX_JOBS": "${MAX_JOBS}",
"MY_VERSION": "${__version__}",
},
"git_options": {
"submodules": False,
Expand Down Expand Up @@ -239,64 +242,86 @@ def test_pbi_test_pkg_extra_environ(
"EXTRA_MAX_JOBS": "1",
}

version = Version("1.0.0")
version_env = {
"MY_VERSION": "1.0.0",
}

pbi = testdata_context.settings.package_build_info(TEST_PKG)
result = pbi.get_extra_environ(template_env={"EXTRA": "extra"}, version=version)
assert (
pbi.get_extra_environ(template_env={"EXTRA": "extra"})
result
== {
"EGG": "spam spam",
"EGG_AGAIN": "spam spam",
"QUOTES": "A\"BC'$EGG", # $$EGG is transformed into $EGG
"SPAM": "alot extra",
"DEF": "default",
}
| version_env
| parallel
)
assert "__version__" not in result

result = pbi.get_extra_environ(
template_env={"EXTRA": "extra", "DEF": "nondefault"}, version=version
)
assert (
pbi.get_extra_environ(template_env={"EXTRA": "extra", "DEF": "nondefault"})
result
== {
"EGG": "spam spam",
"EGG_AGAIN": "spam spam",
"QUOTES": "A\"BC'$EGG", # $$EGG is transformed into $EGG
"SPAM": "alot extra",
"DEF": "nondefault",
}
| version_env
| parallel
)
assert "__version__" not in result

testdata_context.settings.variant = Variant("rocm")
pbi = testdata_context.settings.package_build_info(TEST_PKG)
result = pbi.get_extra_environ(template_env={"EXTRA": "extra"}, version=version)
assert (
pbi.get_extra_environ(template_env={"EXTRA": "extra"})
result
== {
"EGG": "spam",
"EGG_AGAIN": "spam",
"QUOTES": "A\"BC'$EGG",
"SPAM": "",
"DEF": "default",
}
| version_env
| parallel
)
assert "__version__" not in result

testdata_context.settings.variant = Variant("cuda")
pbi = testdata_context.settings.package_build_info(TEST_PKG)
result = pbi.get_extra_environ(template_env={"EXTRA": "spam"}, version=version)
assert (
pbi.get_extra_environ(template_env={"EXTRA": "spam"})
result
== {
"EGG": "spam",
"EGG_AGAIN": "spam",
"QUOTES": "A\"BC'$EGG",
"SPAM": "alot spam",
"DEF": "default",
}
| version_env
| parallel
)
assert "__version__" not in result

build_env = build_environment.BuildEnvironment(
testdata_context,
parent_dir=tmp_path,
)
result = pbi.get_extra_environ(
template_env={"EXTRA": "spam", "PATH": "/sbin:/bin"}, build_env=build_env
template_env={"EXTRA": "spam", "PATH": "/sbin:/bin"},
build_env=build_env,
version=version,
)
assert (
result
Expand All @@ -314,8 +339,10 @@ def test_pbi_test_pkg_extra_environ(
"UV_PYTHON": str(build_env.python),
"UV_PYTHON_DOWNLOADS": "never",
}
| version_env
| parallel
)
assert "__version__" not in result


def test_pbi_test_pkg(testdata_context: context.WorkContext) -> None:
Expand Down Expand Up @@ -805,3 +832,67 @@ def test_use_pypi_org_metadata(testdata_context: context.WorkContext) -> None:
"somepackage_without_customization"
)
assert pbi.use_pypi_org_metadata


def _make_pbi(env_yaml: str, tmp_path: pathlib.Path) -> PackageBuildInfo:
"""Create a PackageBuildInfo from inline env YAML."""
ps = PackageSettings.from_string("version-test-pkg", env_yaml)
settings = Settings(
settings=SettingsFile(),
package_settings=[ps],
variant="cpu",
patches_dir=tmp_path,
max_jobs=1,
)
return settings.package_build_info("version-test-pkg")


def test_version_env_var_raises_when_version_unknown(
tmp_path: pathlib.Path,
) -> None:
"""Using ${__version__} in env without a fallback raises when version is None.

This mirrors the git-URL bootstrap path where the version has not yet
been resolved (e.g. ``pkg @ git+https://host/repo.git@main``).
"""
pbi = _make_pbi(
"""
env:
MY_VERSION: "${__version__}"
""",
tmp_path,
)
with pytest.raises(ValueError, match="__version__"):
pbi.get_extra_environ(template_env={}, version=None)


def test_version_env_var_with_default_when_version_unknown(
tmp_path: pathlib.Path,
) -> None:
"""${__version__:-fallback} substitutes the default when version is None."""
pbi = _make_pbi(
"""
env:
MY_VERSION: "${__version__:-unresolved}"
""",
tmp_path,
)
result = pbi.get_extra_environ(template_env={}, version=None)
assert result["MY_VERSION"] == "unresolved"
assert "__version__" not in result


def test_version_none_no_reference(
tmp_path: pathlib.Path,
) -> None:
"""version=None works when no env vars reference __version__."""
pbi = _make_pbi(
"""
env:
FOO: "bar"
""",
tmp_path,
)
result = pbi.get_extra_environ(template_env={}, version=None)
assert result["FOO"] == "bar"
assert "__version__" not in result
Loading
Loading