How it works¶
Architecture¶
Each concern has its own subpackage, and only the adapters know a build system or the command line. The hatchling hook
and the embed and archive commands gather the options, the build scripts and the build facts (time, revision,
toolchain), and hand them, with a ContentProvider, to the pipeline for their artifact. Each
pipeline opens its artifact once, reads it, gives the provider a ProviderContext that reads from
the same open artifact, and maps what it read to components; one function of the model then adds what every artifact
of the project shares: supplier, source links, revision, tools, dependencies and formulation.
The diagram follows the imports between the subpackages: a solid arrow points at what a package imports when it loads, a dotted arrow labeled lazy at what it imports only once a function needs it, and a dotted arrow labeled types at what it imports only for type annotations. The command line loads each command’s module, and what it imports, only when that command runs.
flowchart TB
subgraph adapters[Adapters]
hook[hatch<br/>hatchling build hook]
cli[_cli<br/>embed, archive, spdx, validate]
end
api[python_sbom<br/>provider API, __main__]
generate[_generate<br/>wheel and archive pipelines]
collect[_collect<br/>settings, facts, revision, project tree, wheels, archives, providers]
validate[_validate<br/>limits, schema, references, licenses]
formats[_formats<br/>CycloneDX JSON, SPDX 2.3]
wheel[_wheel<br/>artifact reader, zip reads and writes, RECORD, bundled wheel checks]
model[_model<br/>inputs, types, identity, graph rules, document building]
errors[_errors<br/>SbomError]
version[version]
hook --> collect
hook -. lazy .-> generate
hook -. types .-> model
cli --> generate
cli --> validate
cli --> formats
cli --> version
cli -. types .-> model
api --> cli
api --> model
generate --> collect
generate --> formats
generate --> wheel
generate --> model
collect --> wheel
collect --> model
validate --> formats
validate --> wheel
validate --> model
formats --> model
formats --> version
model --> errors
wheel --> model
classDef edge fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a;
classDef core fill:#d8f3dc,stroke:#2d6a4f,color:#081c15;
class hook,cli,collect,api edge;
class generate,validate,formats,wheel,model,errors,version core;
Every package raises SbomError, which the command line reports without a traceback; the diagram
draws only the model’s import of it, since the model imports nothing else of the package. Only the hook imports
hatchling, so the provider API and the command line work without it.
The one extension point is the ContentProvider, which every entry point loads from the
content-provider option. A hatchling custom hook can override
content_provider() to build the provider itself.
What the SBOM records¶
The document’s root component is the project, described from the wheel’s METADATA: name, version, summary, license
expression, maintainers, project URLs, classifiers and keywords. Links to the PyPI release, the wheel’s attestation and,
with a repository, the license file follow. A build from a git checkout adds the commit and a link to that exact
revision. Links to the license file and build scripts point at that commit when the checkout is clean, and at the
default branch otherwise.
CycloneDX requires each URL to be an IRI reference. A project URL or home page that is not one gets percent-encoded,
or left out when encoding cannot mend it, as with an scp-style git@host:path address; either way a
python:project-url or python:home-page property keeps the text as written.
The components below the root are the wheels the project bundles and the runtime dependencies it declares. A bundled
wheel carries its SHA-256, its license, a component per file from its RECORD, and the packages it vendors. A
declared dependency carries no version, because the installer picks it at install time; the document marks the root’s
dependencies incomplete for that reason. An extra’s dependencies are left out.
The formulation records how the wheel was built: the interpreter’s version and implementation, every distribution of the
build environment with its dependencies, the files and hashes of each pure-Python one, and SOURCE_DATE_EPOCH when it
set the build time.
Property names start with the project’s normalized name, such as virtualenv:bundled-wheel, or with python: for
values copied from core metadata.
Reproducibility¶
The document holds nothing about the build machine. Its timestamp comes from SOURCE_DATE_EPOCH. When that is unset,
a reproducible hatchling build uses hatchling’s fixed 2020-02-02, the time it stamps on the wheel’s entries, and any
other build uses the current time. Its serial number derives from the content, so identical documents share one and any
difference gets a new one.
The build tools are listed down to their files, so a rebuild can check it installed the same bytes. Only what is the same on every machine goes in: platform wheels appear without their files, because those differ per operating system and architecture, and editable installs without theirs, because they point at a checkout. Installer metadata, console-script launchers and uv’s cache files are left out too, since they name the frontend, the environment’s path or a time rather than the distribution.
Hashing each tool’s RECORD instead would be smaller, but that file lists the installer metadata, so two frontends
installing the same tools would give two hashes. The cost is size: the file list grows with the build environment, and
a large build tool such as setuptools adds hundreds of entries.
So a rebuild reproduces the wheel byte for byte on any operating system when these match the original build: the source
tree, as a checkout of the same commit; SOURCE_DATE_EPOCH; the Python version; and the versions of the build backend
and its dependencies. The SBOM records the last three, so it says what to install before rebuilding.
Limitations¶
The document describes what the project declares and bundles, not the full tree of runtime dependencies, which depends on the environment the wheel is installed into.
The links to the PyPI release and attestation assume the project publishes to PyPI with attestations.
A wheel built from an sdist of a dirty checkout names no commit, since no commit reproduces it.
python-sbom embedandarchiverecord the packages of the environment--build-envnames, by default the one they run in, and always the interpreter they run on. Unless that is the environment the artifact was built in, the build tool record does not match the real build.The SPDX rendering leaves out per-file hashes and the build record, which SPDX 2.3 cannot hold.
A tool that renames the wheel after the build, such as
auditwheel repair, leaves the attestation link pointing at the old file name untilpython-sbom embedruns on the renamed wheel.