Skip to content

Generate minimal SBOM#977

Open
mprpic wants to merge 2 commits intopython-wheel-build:mainfrom
mprpic:generate-sbom
Open

Generate minimal SBOM#977
mprpic wants to merge 2 commits intopython-wheel-build:mainfrom
mprpic:generate-sbom

Conversation

@mprpic
Copy link
Copy Markdown
Contributor

@mprpic mprpic commented Mar 24, 2026

This PR adds logging for discovering existing SBOMs in wheel being built, and a minimal implementation of a Fromager-specific SBOM. The values are set to Red Hat-specific metadata right now, but can be exposed via configuration to arbitrary values in a future PR (or I can tack on that change here as well if necessary).

@mprpic mprpic requested a review from a team as a code owner March 24, 2026 01:50
@mergify mergify bot added the ci label Mar 24, 2026
@mprpic
Copy link
Copy Markdown
Contributor Author

mprpic commented Mar 25, 2026

@lcarva fyi, in case you (or someone else from the Calunga team) wanted to give this a looksy as well.

@python-wheel-build python-wheel-build deleted a comment from tiran Mar 25, 2026
@mprpic mprpic force-pushed the generate-sbom branch 2 times, most recently from e1fe09a to 7cdcffb Compare March 26, 2026 12:35
Copy link
Copy Markdown

@lcarva lcarva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no power here, but this LGTM!

@mprpic
Copy link
Copy Markdown
Contributor Author

mprpic commented Mar 27, 2026

Tested locally in Builder:

main!bootstrap-output.cpu-ubi9/wheels-repo/downloads [*] > d
diff --git a/overrides/settings.yaml b/overrides/settings.yaml
index b452bc03..29a12a52 100644
--- a/overrides/settings.yaml
+++ b/overrides/settings.yaml
@@ -1,4 +1,10 @@
 ---
+sbom:
+  supplier: "Organization: Red Hat"
+  namespace: "https://www.redhat.com"
+  creators:
+    - "Organization: Red Hat"
+
[...truncated...]
main!bootstrap-output.cpu-ubi9/wheels-repo/downloads [*] > ccat markupsafe-3.0.3.dist-info/sboms/fromager.spdx.json
{
  "spdxVersion": "SPDX-2.3",
  "dataLicense": "CC0-1.0",
  "SPDXID": "SPDXRef-DOCUMENT",
  "name": "markupsafe-3.0.3",
  "documentNamespace": "https://www.redhat.com/markupsafe-3.0.3.spdx.json",
  "creationInfo": {
    "created": "2026-03-27T19:42:02Z",
    "creators": [
      "Organization: Red Hat",
      "Tool: fromager-0.1.dev2028+g7cdcffb44"
    ]
  },
  "packages": [
    {
      "SPDXID": "SPDXRef-wheel",
      "name": "markupsafe",
      "versionInfo": "3.0.3",
      "downloadLocation": "NOASSERTION",
      "supplier": "Organization: Red Hat"
    }
  ],
  "relationships": [
    {
      "spdxElementId": "SPDXRef-DOCUMENT",
      "relationshipType": "DESCRIBES",
      "relatedSpdxElement": "SPDXRef-wheel"
    }
  ]
}

Copy link
Copy Markdown
Contributor

@rd4398 rd4398 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks okay to me! I had a few comments / suggestions.

Log any existing SBOM files in .dist-info/sboms/ at INFO level
during wheel metadata injection. This provides visibility into upstream
SBOMs (e.g. from maturin) before Fromager generates its own.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Martin Prpič <mprpic@redhat.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Adds SPDX 2.3 SBOM generation and writing for wheels. Introduces a new SbomSettings model and exposes it from the packagesettings package, and adds an optional per-package purl to PackageSettings. Adds sbom.py with generate_sbom and write_sbom to build SPDX JSON (including optional externalRefs when a PURL is provided). Wires SBOM generation into wheel processing (writes to .dist-info/sboms/fromager.spdx.json) and updates tests to cover model changes, SBOM generation, writing, and wheel integration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Generate minimal SBOM' directly reflects the main change—implementing SBOM generation—and is concise and clear.
Description check ✅ Passed The description accurately covers the two main aspects of the PR: logging for existing SBOMs and a minimal SBOM implementation, with metadata defaults mentioned.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Generate and embed SPDX 2.3 SBOM documents in built wheels per PEP770.
SBOM generation is enabled by adding an `sbom` section to settings.yaml
with supplier, namespace, and creators fields. When absent, no SBOMs
are generated.

- Add SbomSettings model (supplier, namespace, creators) to settings.yaml
- Add per-package purl override with {name}/{version} format substitution
- Create sbom module that generates and writes out SBOMs
- Wire into add_extra_metadata_to_wheels() gated by sbom settings presence
- Write fromager.spdx.json to .dist-info/sboms/, preserving existing SBOMs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Martin Prpič <mprpic@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/fromager/sbom.py (1)

116-133: Add docstring note about overwrite behavior.

Per past review discussion: the function will overwrite an existing fromager.spdx.json if present. Consider adding a brief note to the docstring explaining this is expected since Fromager generates exactly one SBOM per wheel build.

📝 Suggested docstring update
 def write_sbom(
     *,
     sbom: dict[str, typing.Any],
     dist_info_dir: pathlib.Path,
 ) -> pathlib.Path:
     """Write an SBOM document to the .dist-info/sboms/ directory.
 
     Creates the sboms/ subdirectory if it does not already exist.
     Returns the path to the written file.
+
+    Note: This will overwrite any existing fromager.spdx.json file,
+    which is expected since Fromager generates exactly one SBOM per
+    wheel build.
     """
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/sbom.py` around lines 116 - 133, The docstring for write_sbom
currently omits that it will overwrite an existing SBOM file; update the
function's docstring (write_sbom) to explicitly state that writing to
dist_info_dir / "sboms" / SBOM_FILENAME will replace any existing file (this is
expected because Fromager generates a single SBOM per wheel build), so callers
should not rely on preservation of previous files. Include a brief one-line note
about overwrite behavior in the existing docstring.
tests/test_sbom.py (1)

17-33: Avoid mutating private Settings internals in test setup.

Line 33 directly writes to settings._package_settings, which makes tests brittle if internals change. Prefer passing package settings through the public constructor argument.

♻️ Proposed refactor
 def _make_ctx(
     tmp_path: pathlib.Path,
     sbom_settings: SbomSettings | None = None,
     purl: str | None = None,
 ) -> context.WorkContext:
     """Create a minimal WorkContext with SBOM settings."""
+    package_settings: list[packagesettings.PackageSettings] = []
+    if purl is not None:
+        package_settings.append(
+            packagesettings.PackageSettings.from_mapping(
+                "test-pkg",
+                {"purl": purl},
+                source="test",
+                has_config=True,
+            )
+        )
+
     settings_file = packagesettings.SettingsFile(sbom=sbom_settings)
     settings = packagesettings.Settings(
         settings=settings_file,
-        package_settings=[],
+        package_settings=package_settings,
         patches_dir=tmp_path / "patches",
         variant="cpu",
         max_jobs=None,
     )
-    if purl is not None:
-        # Create a package setting with purl override
-        ps = packagesettings.PackageSettings.from_mapping(
-            "test-pkg",
-            {"purl": purl},
-            source="test",
-            has_config=True,
-        )
-        settings._package_settings[ps.name] = ps
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_sbom.py` around lines 17 - 33, Test mutates private internals by
writing to settings._package_settings; instead construct the Settings with
package settings provided publicly: create the PackageSettings via
packagesettings.PackageSettings.from_mapping(...) and pass it in the
package_settings list to packagesettings.Settings(...) (use SettingsFile /
Settings constructors shown) instead of assigning to the private attribute
_package_settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/fromager/sbom.py`:
- Around line 116-133: The docstring for write_sbom currently omits that it will
overwrite an existing SBOM file; update the function's docstring (write_sbom) to
explicitly state that writing to dist_info_dir / "sboms" / SBOM_FILENAME will
replace any existing file (this is expected because Fromager generates a single
SBOM per wheel build), so callers should not rely on preservation of previous
files. Include a brief one-line note about overwrite behavior in the existing
docstring.

In `@tests/test_sbom.py`:
- Around line 17-33: Test mutates private internals by writing to
settings._package_settings; instead construct the Settings with package settings
provided publicly: create the PackageSettings via
packagesettings.PackageSettings.from_mapping(...) and pass it in the
package_settings list to packagesettings.Settings(...) (use SettingsFile /
Settings constructors shown) instead of assigning to the private attribute
_package_settings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a88eb459-5fe5-4470-8a85-fa2f3b555d69

📥 Commits

Reviewing files that changed from the base of the PR and between aec9c9c and d8a87b4.

📒 Files selected for processing (9)
  • src/fromager/packagesettings/__init__.py
  • src/fromager/packagesettings/_models.py
  • src/fromager/packagesettings/_pbi.py
  • src/fromager/packagesettings/_settings.py
  • src/fromager/sbom.py
  • src/fromager/wheels.py
  • tests/test_packagesettings.py
  • tests/test_sbom.py
  • tests/test_wheels.py

@mprpic mprpic requested review from rd4398 and tiran March 31, 2026 18:09
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fromager/sbom.py`:
- Around line 41-43: The purl_override.format(...) call can raise
KeyError/ValueError for malformed templates; wrap the .format(name=package_name,
version=package_version) call in a try/except that catches KeyError and
ValueError and re-raises a new ValueError with a clear message including the
offending template (purl_override) and allowed placeholders ("name" and
"version") plus package_name/package_version for context; ensure the original
exception is chained (raise ... from e) so traceback is preserved and return
None only when purl_override is falsy as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e8787098-25e1-4989-b849-3e0e8e491b67

📥 Commits

Reviewing files that changed from the base of the PR and between d8a87b4 and 5592519.

📒 Files selected for processing (9)
  • src/fromager/packagesettings/__init__.py
  • src/fromager/packagesettings/_models.py
  • src/fromager/packagesettings/_pbi.py
  • src/fromager/packagesettings/_settings.py
  • src/fromager/sbom.py
  • src/fromager/wheels.py
  • tests/test_packagesettings.py
  • tests/test_sbom.py
  • tests/test_wheels.py
✅ Files skipped from review due to trivial changes (2)
  • tests/test_packagesettings.py
  • tests/test_sbom.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/fromager/packagesettings/_pbi.py
  • src/fromager/packagesettings/_settings.py
  • tests/test_wheels.py
  • src/fromager/wheels.py
  • src/fromager/packagesettings/init.py

Comment on lines +41 to +43
if purl_override:
return purl_override.format(name=package_name, version=package_version)
return None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify direct template expansion site and whether malformed-template handling exists.
rg -n -C3 'purl_override\.format\(' src/fromager/sbom.py
rg -n -C2 'invalid purl template|supported fields|KeyError|ValueError' src/fromager/sbom.py tests/test_sbom.py || true

Repository: python-wheel-build/fromager

Length of output: 239


Handle malformed PURL templates explicitly.

Line 42 directly calls .format() without error handling. Malformed templates (e.g., unknown placeholders, bad braces) will raise KeyError or ValueError with unclear context. Wrap it in a try-catch and re-raise with a helpful message per coding guidelines.

Suggested fix
     if purl_override:
-        return purl_override.format(name=package_name, version=package_version)
+        try:
+            return purl_override.format(name=package_name, version=str(package_version))
+        except (KeyError, ValueError) as err:
+            raise ValueError(
+                f"invalid purl template {purl_override!r}; supported fields are "
+                "{name} and {version}"
+            ) from err
     return None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if purl_override:
return purl_override.format(name=package_name, version=package_version)
return None
if purl_override:
try:
return purl_override.format(name=package_name, version=str(package_version))
except (KeyError, ValueError) as err:
raise ValueError(
f"invalid purl template {purl_override!r}; supported fields are "
"{name} and {version}"
) from err
return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/sbom.py` around lines 41 - 43, The purl_override.format(...)
call can raise KeyError/ValueError for malformed templates; wrap the
.format(name=package_name, version=package_version) call in a try/except that
catches KeyError and ValueError and re-raises a new ValueError with a clear
message including the offending template (purl_override) and allowed
placeholders ("name" and "version") plus package_name/package_version for
context; ensure the original exception is chained (raise ... from e) so
traceback is preserved and return None only when purl_override is falsy as
before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants