From 23b30ab116095f471b2e03fcf231c7ed7baadcfb Mon Sep 17 00:00:00 2001 From: Andreas Fehlner Date: Sun, 10 May 2026 15:30:38 +0200 Subject: [PATCH 1/4] feat: Add PEP 770 SBOM files support (wheel.sbom-files setting) - Add wheel.sbom-files configuration option for including SBOM files in wheels - SBOM files are copied to *.dist-info/sboms/ directory per PEP 770 - Support for override inheritance via tool.scikit-build.overrides - Validate SBOM file existence at build time - Add comprehensive tests for settings parsing, overrides, and wheel output --- docs/reference/configs.md | 9 ++++++ src/scikit_build_core/build/wheel.py | 13 +++++++++ .../resources/scikit-build.schema.json | 10 +++++++ .../settings/skbuild_model.py | 7 +++++ tests/test_pyproject_pep517.py | 28 +++++++++++++++++++ tests/test_settings_overrides.py | 6 ++++ tests/test_skbuild_settings.py | 2 ++ 7 files changed, 75 insertions(+) diff --git a/docs/reference/configs.md b/docs/reference/configs.md index d1b690672..e100f0184 100644 --- a/docs/reference/configs.md +++ b/docs/reference/configs.md @@ -571,6 +571,15 @@ print(mk_skbuild_docs()) Must not be set if ``project.license-files`` is set. ``` +```{eval-rst} +.. confval:: wheel.sbom-files + :type: ``list[str]`` + + A list of Software Bill of Materials files to include in the wheel. + + Files are copied into the ``*.dist-info/sboms`` directory. +``` + ```{eval-rst} .. confval:: wheel.packages :type: ``list[str]`` diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py index ea8fb2ff6..b63ff29bd 100644 --- a/src/scikit_build_core/build/wheel.py +++ b/src/scikit_build_core/build/wheel.py @@ -364,6 +364,19 @@ def _build_wheel_impl_impl( "No license files found, set wheel.license-files to [] to suppress this warning" ) + sbom_paths: list[Path] = [] + for sbom_file in settings.wheel.sbom_files or []: + sbom_path = Path(sbom_file) + if not sbom_path.is_file(): + msg = f"SBOM file not found: {sbom_file}" + raise FileNotFoundError(msg) + sbom_paths.append(sbom_path) + + for sbom_path in sbom_paths: + path = wheel_dirs["metadata"] / "sboms" / sbom_path.name + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(sbom_path, path) + for gen in settings.generate: if gen.location == "source": contents = generate_file_contents(gen, metadata) diff --git a/src/scikit_build_core/resources/scikit-build.schema.json b/src/scikit_build_core/resources/scikit-build.schema.json index 3336845f9..7188b5805 100644 --- a/src/scikit_build_core/resources/scikit-build.schema.json +++ b/src/scikit_build_core/resources/scikit-build.schema.json @@ -237,6 +237,13 @@ }, "description": "A list of license files to include in the wheel. Supports glob patterns." }, + "sbom-files": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of Software Bill of Materials files to include in the wheel." + }, "cmake": { "type": "boolean", "default": true, @@ -585,6 +592,9 @@ "license-files": { "$ref": "#/$defs/inherit" }, + "sbom-files": { + "$ref": "#/$defs/inherit" + }, "exclude": { "$ref": "#/$defs/inherit" }, diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index 567fa0f97..5a5551d7d 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -331,6 +331,13 @@ class WheelSettings: Must not be set if ``project.license-files`` is set. """ + sbom_files: Optional[List[str]] = None + """ + A list of Software Bill of Materials files to include in the wheel. + + Files are copied into the ``*.dist-info/sboms`` directory. + """ + cmake: bool = True """ Run CMake as part of building the wheel. diff --git a/tests/test_pyproject_pep517.py b/tests/test_pyproject_pep517.py index c2936dea6..519914de4 100644 --- a/tests/test_pyproject_pep517.py +++ b/tests/test_pyproject_pep517.py @@ -313,6 +313,34 @@ def test_pep517_wheel_source_dir(virtualenv, tmp_path: Path): assert add.strip() == "3" +@pytest.mark.parametrize("package", ["simple_purelib_package"], indirect=True) +@pytest.mark.usefixtures("package") +def test_pep517_wheel_sbom_files(tmp_path: Path): + sbom_dir = Path("sbom_inputs") + sbom_dir.mkdir() + sbom_file = sbom_dir / "project.spdx.json" + sbom_content = '{"spdxVersion": "SPDX-2.3"}' + sbom_file.write_text(sbom_content, encoding="utf-8") + + dist = tmp_path / "dist" + build_wheel(str(dist), {"wheel.sbom-files": [str(sbom_file)]}) + + (wheel,) = dist.glob("purelib_example-0.0.1-*.whl") + wheel = wheel.resolve() + with zipfile.ZipFile(wheel) as zf: + sbom_path = "purelib_example-0.0.1.dist-info/sboms/project.spdx.json" + assert sbom_path in set(zf.namelist()) + assert zf.read(sbom_path).decode("utf-8") == sbom_content + + +@pytest.mark.parametrize("package", ["simple_purelib_package"], indirect=True) +@pytest.mark.usefixtures("package") +def test_pep517_wheel_sbom_file_missing(tmp_path: Path): + dist = tmp_path / "dist" + with pytest.raises(FileNotFoundError, match="SBOM file not found: missing.spdx.json"): + build_wheel(str(dist), {"wheel.sbom-files": ["missing.spdx.json"]}) + + @pytest.mark.skip(reason="Doesn't work yet") @pytest.mark.compile @pytest.mark.configure diff --git a/tests/test_settings_overrides.py b/tests/test_settings_overrides.py index 9657c8476..4c83d6219 100644 --- a/tests/test_settings_overrides.py +++ b/tests/test_settings_overrides.py @@ -596,6 +596,7 @@ def test_skbuild_overrides_inherit(inherit: str, tmp_path: Path): cmake.targets = ["a", "b"] wheel.packages = ["a", "b"] wheel.license-files = ["a.txt", "b.txt"] + wheel.sbom-files = ["a.spdx.json", "b.cdx.json"] wheel.exclude = ["x", "y"] install.components = ["a", "b"] cmake.define = {{a="A", b="B"}} @@ -606,6 +607,7 @@ def test_skbuild_overrides_inherit(inherit: str, tmp_path: Path): inherit.cmake.targets = "{inherit}" inherit.wheel.packages = "{inherit}" inherit.wheel.license-files = "{inherit}" + inherit.wheel.sbom-files = "{inherit}" inherit.wheel.exclude = "{inherit}" inherit.install.components = "{inherit}" inherit.cmake.define = "{inherit}" @@ -613,6 +615,7 @@ def test_skbuild_overrides_inherit(inherit: str, tmp_path: Path): cmake.targets = ["c", "d"] wheel.packages = ["c", "d"] wheel.license-files = ["c.txt", "d.txt"] + wheel.sbom-files = ["c.spdx.json", "d.cdx.json"] wheel.exclude = ["xx", "yy"] install.components = ["c", "d"] cmake.define = {{b="X", c="C"}} @@ -629,6 +632,7 @@ def test_skbuild_overrides_inherit(inherit: str, tmp_path: Path): assert settings.cmake.targets == ["c", "d"] assert settings.wheel.packages == ["c", "d"] assert settings.wheel.license_files == ["c.txt", "d.txt"] + assert settings.wheel.sbom_files == ["c.spdx.json", "d.cdx.json"] assert settings.wheel.exclude == ["xx", "yy"] assert settings.install.components == ["c", "d"] assert settings.cmake.define == {"b": "X", "c": "C"} @@ -637,6 +641,7 @@ def test_skbuild_overrides_inherit(inherit: str, tmp_path: Path): assert settings.cmake.targets == ["a", "b", "c", "d"] assert settings.wheel.packages == ["a", "b", "c", "d"] assert settings.wheel.license_files == ["a.txt", "b.txt", "c.txt", "d.txt"] + assert settings.wheel.sbom_files == ["a.spdx.json", "b.cdx.json", "c.spdx.json", "d.cdx.json"] assert settings.wheel.exclude == ["x", "y", "xx", "yy"] assert settings.install.components == ["a", "b", "c", "d"] assert settings.cmake.define == {"a": "A", "b": "X", "c": "C"} @@ -645,6 +650,7 @@ def test_skbuild_overrides_inherit(inherit: str, tmp_path: Path): assert settings.cmake.targets == ["c", "d", "a", "b"] assert settings.wheel.packages == ["c", "d", "a", "b"] assert settings.wheel.license_files == ["c.txt", "d.txt", "a.txt", "b.txt"] + assert settings.wheel.sbom_files == ["c.spdx.json", "d.cdx.json", "a.spdx.json", "b.cdx.json"] assert settings.wheel.exclude == ["xx", "yy", "x", "y"] assert settings.install.components == ["c", "d", "a", "b"] assert settings.cmake.define == {"a": "A", "b": "B", "c": "C"} diff --git a/tests/test_skbuild_settings.py b/tests/test_skbuild_settings.py index 48af6109c..f737181e4 100644 --- a/tests/test_skbuild_settings.py +++ b/tests/test_skbuild_settings.py @@ -187,6 +187,7 @@ def test_skbuild_settings_config_settings( "wheel.py-api": "cp39", "wheel.expand-macos-universal-tags": "True", "wheel.license-files": ["a", "b", "c"], + "wheel.sbom-files": ["sbom1.spdx.json", "sbom2.cdx.json"], "wheel.exclude": ["b", "y", "e"], "wheel.build-tag": "1foo", "backport.find-python": "0", @@ -231,6 +232,7 @@ def test_skbuild_settings_config_settings( assert settings.wheel.py_api == "cp39" assert settings.wheel.expand_macos_universal_tags assert settings.wheel.license_files == ["a", "b", "c"] + assert settings.wheel.sbom_files == ["sbom1.spdx.json", "sbom2.cdx.json"] assert settings.wheel.exclude == ["b", "y", "e"] assert settings.wheel.build_tag == "1foo" assert settings.backport.find_python == Version("0") From 2ba615ca7c5e9bcd366304efedf5535e2040e1b2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 15:41:42 +0000 Subject: [PATCH 2/4] style: pre-commit fixes --- tests/test_pyproject_pep517.py | 4 +++- tests/test_settings_overrides.py | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_pyproject_pep517.py b/tests/test_pyproject_pep517.py index 519914de4..7c9cf9a23 100644 --- a/tests/test_pyproject_pep517.py +++ b/tests/test_pyproject_pep517.py @@ -337,7 +337,9 @@ def test_pep517_wheel_sbom_files(tmp_path: Path): @pytest.mark.usefixtures("package") def test_pep517_wheel_sbom_file_missing(tmp_path: Path): dist = tmp_path / "dist" - with pytest.raises(FileNotFoundError, match="SBOM file not found: missing.spdx.json"): + with pytest.raises( + FileNotFoundError, match="SBOM file not found: missing.spdx.json" + ): build_wheel(str(dist), {"wheel.sbom-files": ["missing.spdx.json"]}) diff --git a/tests/test_settings_overrides.py b/tests/test_settings_overrides.py index 4c83d6219..9d0c9d3f7 100644 --- a/tests/test_settings_overrides.py +++ b/tests/test_settings_overrides.py @@ -641,7 +641,12 @@ def test_skbuild_overrides_inherit(inherit: str, tmp_path: Path): assert settings.cmake.targets == ["a", "b", "c", "d"] assert settings.wheel.packages == ["a", "b", "c", "d"] assert settings.wheel.license_files == ["a.txt", "b.txt", "c.txt", "d.txt"] - assert settings.wheel.sbom_files == ["a.spdx.json", "b.cdx.json", "c.spdx.json", "d.cdx.json"] + assert settings.wheel.sbom_files == [ + "a.spdx.json", + "b.cdx.json", + "c.spdx.json", + "d.cdx.json", + ] assert settings.wheel.exclude == ["x", "y", "xx", "yy"] assert settings.install.components == ["a", "b", "c", "d"] assert settings.cmake.define == {"a": "A", "b": "X", "c": "C"} @@ -650,7 +655,12 @@ def test_skbuild_overrides_inherit(inherit: str, tmp_path: Path): assert settings.cmake.targets == ["c", "d", "a", "b"] assert settings.wheel.packages == ["c", "d", "a", "b"] assert settings.wheel.license_files == ["c.txt", "d.txt", "a.txt", "b.txt"] - assert settings.wheel.sbom_files == ["c.spdx.json", "d.cdx.json", "a.spdx.json", "b.cdx.json"] + assert settings.wheel.sbom_files == [ + "c.spdx.json", + "d.cdx.json", + "a.spdx.json", + "b.cdx.json", + ] assert settings.wheel.exclude == ["xx", "yy", "x", "y"] assert settings.install.components == ["c", "d", "a", "b"] assert settings.cmake.define == {"a": "A", "b": "B", "c": "C"} From 4212e506ddec5767f1657f88e41111cd50ebdd18 Mon Sep 17 00:00:00 2001 From: Andreas Fehlner Date: Sun, 10 May 2026 17:50:56 +0200 Subject: [PATCH 3/4] update Signed-off-by: Andreas Fehlner --- tests/test_pyproject_pep517.py | 28 ---------------------- tests/test_pyproject_pep770.py | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 tests/test_pyproject_pep770.py diff --git a/tests/test_pyproject_pep517.py b/tests/test_pyproject_pep517.py index 519914de4..c2936dea6 100644 --- a/tests/test_pyproject_pep517.py +++ b/tests/test_pyproject_pep517.py @@ -313,34 +313,6 @@ def test_pep517_wheel_source_dir(virtualenv, tmp_path: Path): assert add.strip() == "3" -@pytest.mark.parametrize("package", ["simple_purelib_package"], indirect=True) -@pytest.mark.usefixtures("package") -def test_pep517_wheel_sbom_files(tmp_path: Path): - sbom_dir = Path("sbom_inputs") - sbom_dir.mkdir() - sbom_file = sbom_dir / "project.spdx.json" - sbom_content = '{"spdxVersion": "SPDX-2.3"}' - sbom_file.write_text(sbom_content, encoding="utf-8") - - dist = tmp_path / "dist" - build_wheel(str(dist), {"wheel.sbom-files": [str(sbom_file)]}) - - (wheel,) = dist.glob("purelib_example-0.0.1-*.whl") - wheel = wheel.resolve() - with zipfile.ZipFile(wheel) as zf: - sbom_path = "purelib_example-0.0.1.dist-info/sboms/project.spdx.json" - assert sbom_path in set(zf.namelist()) - assert zf.read(sbom_path).decode("utf-8") == sbom_content - - -@pytest.mark.parametrize("package", ["simple_purelib_package"], indirect=True) -@pytest.mark.usefixtures("package") -def test_pep517_wheel_sbom_file_missing(tmp_path: Path): - dist = tmp_path / "dist" - with pytest.raises(FileNotFoundError, match="SBOM file not found: missing.spdx.json"): - build_wheel(str(dist), {"wheel.sbom-files": ["missing.spdx.json"]}) - - @pytest.mark.skip(reason="Doesn't work yet") @pytest.mark.compile @pytest.mark.configure diff --git a/tests/test_pyproject_pep770.py b/tests/test_pyproject_pep770.py new file mode 100644 index 000000000..5ec5d9869 --- /dev/null +++ b/tests/test_pyproject_pep770.py @@ -0,0 +1,44 @@ +import zipfile +from pathlib import Path + +import pytest + +from scikit_build_core.build import build_wheel + +SPDX_CONTENT = '{"spdxVersion": "SPDX-2.3", "SPDXID": "SPDXRef-DOCUMENT"}' +CYCLONEDX_CONTENT = '{"bomFormat": "CycloneDX", "specVersion": "1.5"}' + + +@pytest.mark.parametrize( + ("filename", "content"), + [ + ("project.spdx.json", SPDX_CONTENT), + ("bom.json", CYCLONEDX_CONTENT), + ], + ids=["spdx", "cyclonedx"], +) +@pytest.mark.parametrize("package", ["simple_purelib_package"], indirect=True) +@pytest.mark.usefixtures("package") +def test_pep770_wheel_sbom_files(tmp_path: Path, filename: str, content: str): + sbom_dir = Path("sbom_inputs") + sbom_dir.mkdir() + sbom_file = sbom_dir / filename + sbom_file.write_text(content, encoding="utf-8") + + dist = tmp_path / "dist" + build_wheel(str(dist), {"wheel.sbom-files": [str(sbom_file)]}) + + (wheel,) = dist.glob("purelib_example-0.0.1-*.whl") + wheel = wheel.resolve() + with zipfile.ZipFile(wheel) as zf: + sbom_path = f"purelib_example-0.0.1.dist-info/sboms/{filename}" + assert sbom_path in set(zf.namelist()) + assert zf.read(sbom_path).decode("utf-8") == content + + +@pytest.mark.parametrize("package", ["simple_purelib_package"], indirect=True) +@pytest.mark.usefixtures("package") +def test_pep770_wheel_sbom_file_missing(tmp_path: Path): + dist = tmp_path / "dist" + with pytest.raises(FileNotFoundError, match="SBOM file not found: missing_sbom.json"): + build_wheel(str(dist), {"wheel.sbom-files": ["missing_sbom.json"]}) From 87bbd250f78e993d55d03b4a07ada7c0966ace67 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 16:04:24 +0000 Subject: [PATCH 4/4] style: pre-commit fixes --- tests/test_pyproject_pep770.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_pyproject_pep770.py b/tests/test_pyproject_pep770.py index 09ee993b8..bd912d980 100644 --- a/tests/test_pyproject_pep770.py +++ b/tests/test_pyproject_pep770.py @@ -40,5 +40,7 @@ def test_pep770_wheel_sbom_files(tmp_path: Path, filename: str, content: str): @pytest.mark.usefixtures("package") def test_pep770_wheel_sbom_file_missing(tmp_path: Path): dist = tmp_path / "dist" - with pytest.raises(FileNotFoundError, match=r"SBOM file not found: missing_sbom\.json"): + with pytest.raises( + FileNotFoundError, match=r"SBOM file not found: missing_sbom\.json" + ): build_wheel(str(dist), {"wheel.sbom-files": ["missing_sbom.json"]})