Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ jobs:
- name: 🔏 Sigstore sign artifacts
uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0
with:
inputs: dist/*.whl dist/*.tar.gz
inputs: dist/*.whl dist/*.tar.gz sbom-python.cdx.json
- name: ⬆️ Attach artifacts to release
env:
GH_TOKEN: ${{ github.token }}
Expand Down
49 changes: 37 additions & 12 deletions scripts/sbom_from_wheel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@ uvx --from "cyclonedx-bom==${CYCLONEDX_BOM_VERSION}" cyclonedx-py environment "$
--mc-type "$MC_TYPE" \
--output-format json -o "$OUT"

# Installed *after* the SBOM is generated, and that order is load-bearing: this
# venv is the environment the document describes, so anything added to it before
# the previous step would be published as a component of the release.
uv pip install --quiet --python "$VENV/bin/python" "packaging==${PACKAGING_VERSION}"

# Run under the venv's interpreter rather than the runner's. The check below
# evaluates environment markers, and the only environment whose answers mean
# anything here is the one the SBOM describes.
"$VENV/bin/python" - "$DIST_DIR" "$OUT" <<'PYEOF'
# Overlaid, not installed. This venv is the environment the document describes,
# so installing into it would publish packaging as a component of the release --
# and where packaging is already in the closure, would silently replace a
# version the document has just recorded. `--with` layers it onto the
# interpreter for this one call and leaves the venv byte-identical, while the
# check still sees the venv's own site-packages: the only environment whose
# marker answers mean anything here.
uv run --no-project --python "$VENV/bin/python" \
--with "packaging==${PACKAGING_VERSION}" \
python - "$DIST_DIR" "$OUT" <<'PYEOF'
"""Complete the root component from the wheel, then verify the SBOM.

Every project here declares `dynamic = ["version"]`, so cyclonedx-py reads the
Expand All @@ -69,6 +70,7 @@ import re
import sys
import zipfile

from packaging.markers import UndefinedComparison, UndefinedEnvironmentName
from packaging.requirements import InvalidRequirement, Requirement

dist, out = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
Expand All @@ -91,6 +93,26 @@ if wheel is None:
raw_name, version = wheel.name.split("-")[:2]
expected = normalize(raw_name)


def installed_here(marker):
"""True if a dependency carrying this marker is installed in this venv.

The wheel above was installed without extras, so anything gated behind one
is legitimately absent. Two probes decide that: if the marker's answer
changes with `extra`, it is extra-gated whatever the operator. Markers
raise at evaluate time, not parse time, so both probes are guarded.
"""
if marker is None:
return True
results = set()
for probe in ("", "\x00no-such-extra"):
try:
results.add(marker.evaluate({"extra": probe}))
except (UndefinedComparison, UndefinedEnvironmentName) as exc:
fail(f"cannot evaluate the environment marker {str(marker)!r}: {exc}")
return False if len(results) > 1 else results.pop()


# Requires-Dist from the wheel's own metadata, narrowed to what this
# environment should actually hold. A marker decides that, so a marker is what
# has to be evaluated -- not a substring of one.
Expand All @@ -104,8 +126,11 @@ expected = normalize(raw_name)
# dependency today; the check is fixed here so that the first one added does not
# break a release to discover it.
#
# An empty `extra` is what makes `extra == "cli"` false: no extra was requested
# when the wheel was installed above.
# Extras are decided by whether the marker's answer depends on `extra` at all,
# not by evaluating it against an empty one. `extra == "cli"` is false for an
# empty extra, but `extra in "cli"` is *true* -- "" is a substring of every
# string -- so the sentinel alone would demand an extra-gated dependency that
# was correctly never installed, and abort the release blaming the venv.
requires = set()
with zipfile.ZipFile(wheel) as zf:
metadata_name = next((n for n in zf.namelist() if n.endswith(".dist-info/METADATA")), None)
Expand All @@ -119,7 +144,7 @@ with zipfile.ZipFile(wheel) as zf:
req = Requirement(spec)
except InvalidRequirement as exc:
fail(f"{wheel.name} has an unparsable Requires-Dist {spec!r}: {exc}")
if req.marker is not None and not req.marker.evaluate({"extra": ""}):
if not installed_here(req.marker):
continue
requires.add(normalize(req.name))

Expand Down
Loading