Conversation
A build now emits a CycloneDX 1.6 SBOM, a third-party notices file and a pinned requirements list, checksummed alongside the bundle and the installer. The set is the runtime dependency closure of the installed package, which is deliberately not the build environment. The release inventory already records every distribution present, and on a developer machine that includes pytest and ruff: right for reproducing a build, wrong as a statement about what is distributed. The closure is resolved from package metadata with the gui extra included and dev excluded, and a package that is required but not installed is an error rather than a silent omission, because a bill of materials that quietly drops what it could not resolve hides the gap it exists to show. Each component records which metadata field its licence came from. The three fields do not carry equal weight: a PEP 639 License-Expression is precise, a classifier's "BSD License" is approximate, and the free-text field is sometimes a paragraph. Prose is marked as loose rather than truncated into something that resembles an SPDX identifier, and only a real expression is emitted as a CycloneDX expression rather than a bare name. Components whose licences carry redistribution conditions beyond attribution are flagged, and the generator lists them on stderr. Qt ships under LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only while Offloader is MIT, so PySide6, PySide6_Essentials, PySide6_Addons and shiboken6 are all flagged. That is a prompt and not a verdict: what those terms require of a frozen bundle is a decision for a person, and this makes it impossible to miss rather than answering it. The notices file says in as many words that it is not legal advice. The SBOM's serial number is derived from its contents, so the same inputs produce the same document and two of them can be diffed to see what actually moved. A random serial would differ on every rebuild in a field nobody meant to compare. The writer and the build's own output validation name these files in one place, since validate_outputs refuses any checksummed artifact it did not expect and the two disagreeing would fail a signed build at its own verification step. The source archive check gains sbom.py too: a release built from an sdist without it could not produce its own bill of materials.
owenpkent
left a comment
There was a problem hiding this comment.
Reviewed dependency-closure resolution, license metadata provenance, deterministic CycloneDX output, build/checksum integration, candidate artifacts, and the release-plan claims. Two integration gaps remain: the generated publication instructions omit the new artifacts, and the package-only inventory does not cover all components in the frozen application. Details inline.
Validation was static against this exact diff and the surrounding build/release scripts. This review did not produce a new Windows bundle or make a redistribution-license determination.
| if path.is_file(): | ||
| output.write(path, f"Offloader/{path.relative_to(bundle).as_posix()}") | ||
| outputs = [archive, inventory] | ||
| outputs = [archive, inventory, *sbom.write_all(DIST, identity)] |
There was a problem hiding this comment.
[P2] Include the new BOM artifacts in the signed-release upload instructions.
These three files now enter SHA256SUMS.txt, but the upload command generated by scripts/release_notes.py:44 and the command in docs/build-windows.md still upload only Setup, SHA256SUMS, and the inventory. Following the supplied release instructions therefore publishes checksums for unavailable SBOM/notices/requirements assets. The notices also do not accompany the release as this PR describes; generating them beside the bundle does not embed them in the existing installer.
Add all three artifact names to both upload instructions, preferably from a shared artifact-name helper. Test the generated release-note list against sbom_names() so adding another required output cannot leave the publication step behind.
| return any(parsed.marker.evaluate({"extra": extra}) for extra in extras) | ||
|
|
||
|
|
||
| def closure(root: str = "offloader", |
There was a problem hiding this comment.
[P2] Retain the incomplete-inventory gate until frozen runtime components are covered.
This closure traverses Python distribution metadata only. The frozen application also ships components that cannot appear in that graph, including the CPython DLL required by bundle validation and the PyInstaller bootloader embedded in the executables. These outputs therefore describe the Python dependency set, not the complete shipped application.
That boundary matters because this PR removes the release-plan item stating that the complete third-party inventory/SBOM is still pending. Either add the frozen runtime components and their versions/provenance using the actual bundle inventory, or keep that release gate and explicitly limit the generated SBOM's claim to Python distribution dependencies. Add a bundle-level check for the runtime entries before calling the complete inventory implemented.
Two ways this described itself as more finished than it is. The three new files entered SHA256SUMS.txt, but the upload command generated by scripts/release_notes.py and the one in docs/build-windows.md still named only the installer, the checksums and the inventory. Following the supplied instructions therefore published checksums for assets that were not there. artifacts.release_assets() is now the one list, read by the build that checksums them, by the verification that refuses anything it did not expect, and by the instructions that upload them, with a test that the generated notes name every file sbom_names() produces. The notices are also not embedded in the installer - generating them beside the bundle does not put them inside an executable that was already assembled - so the docs no longer say they travel with it. The closure walks Python distribution metadata, and a frozen application ships components that have none: the CPython runtime DLL bundle validation requires, and the PyInstaller bootloader compiled into each of the three executables. So these files describe the Python dependency set, not the complete shipped application, and this PR had removed the release-plan item saying a complete inventory was still pending. The gate is back, worded as what actually remains. Every output states its own boundary in its own text - a scope line in the notices with the uncovered components named, an offloader:scope property in the SBOM, a comment in the requirements file - because an inventory read as complete while missing the interpreter it ships is worse than one that says where it stops. uncovered_in_bundle() reports which of those are present in a built bundle, measured against the tree rather than asserted from the list, so the gap shrinks as it is closed and cannot be closed by editing a constant. build.py now puts its own directory on sys.path. Its sibling scripts are imported by name from inside functions, which works when it is run directly and not when it is imported as a module, which is what the tests do. Eight tests: every output carrying its boundary, the SBOM naming each uncovered component, the gap measured against a synthetic bundle, the release plan still carrying the gate, and the generated instructions uploading every checksummed asset.
Summary
A build now emits three files describing what it distributes, checksummed alongside the bundle and installer:
Offloader-{version}-sbom.cyclonedx.jsonOffloader-{version}-third-party-notices.txtOffloader-{version}-requirements.txtThis closes the last release-plan item that did not need the signing token.
The thing this PR actually surfaces
Qt ships under
LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only. Offloader is MIT. PySide6, PySide6_Essentials, PySide6_Addons and shiboken6 are all flagged[review], and the generator exits with them listed on stderr.That is deliberately a prompt and not a verdict. What those terms require of a frozen bundle is a decision for a person; this makes it impossible to miss rather than answering it, and the notices file says in as many words that it is not legal advice. It is the open item I would put in front of you before a public beta.
What it inventories, and what it does not
The set is the runtime dependency closure of the installed package, not the build environment.
build.py's existing inventory already records every distribution present, which on a developer machine includes pytest and ruff: right for reproducing a build, wrong as a statement about what is distributed. So the closure is resolved from metadata with theguiextra in anddevout — 8 packages, transitives included (pillowvia reportlab,shiboken6via PySide6).A package that is required but not installed is an error, not an omission. An SBOM that quietly drops what it could not resolve hides the gap it exists to show.
Precision about licences
Three metadata fields carry a licence and they do not carry equal weight, so each component records which one it came from:
License-Expression— precise, emitted as CycloneDXexpressionClassifier— approximate ("BSD License"), emitted as a barelicense.name, because declaring it an expression asserts an SPDX identifier that does not existLicense— sometimes a paragraph (reportlab's is), marked as loose rather than truncated into something resembling a licence nameThe serial number is derived from the contents, so the same inputs produce the same document and two SBOMs can be diffed to see what moved. A random one would differ on every rebuild in a field nobody meant to compare.
Integration detail worth a look
validate_outputsrefuses any checksummed artifact it did not expect, so the writer and the expectation are named in one place (sbom_names). If they disagreed, a signed build would fail at its own verification step. I also addedsbom.pyto CI's sdist assertion: a release built from an sdist without it could not produce its own bill of materials.Also in this branch's stack: a CI fix
You saw the failure on #15. Root cause was one test of mine in #13,
test_the_target_is_computed_not_read_from_the_registry, which hardcoded a Windows path. A backslash is not a separator off Windows, soinstall_target()resolved the literal against the working directory and it failed on all three non-Windows jobs while passing where I wrote it. Fixed at the source in #13 using a real directory, with the Program Files fallback asserted separately, and #14 and #15 rebased onto it. The first stack (#8 to #12) was green throughout.Test plan
tests/test_sbom.py— 26 tests: the closure boundary (transitives in, dev deps out, offloader not its own component), a missing requirement erroring rather than omitting, name normalisation, licence source recorded per component, copyleft flagged and permissive not, prose marked loose, CycloneDX 1.6 shape with unique bom-refs and purls, expression-vs-name discipline, serial stability and sensitivity, and the writer matchingbuild.sbom_namessbom.pyalongside the other builder filesruff check src tests scripts build/windowscleanvalidate_outputsover the new artifacts needs the token