Skip to content

Latest commit

 

History

History
433 lines (339 loc) · 16.9 KB

File metadata and controls

433 lines (339 loc) · 16.9 KB
Created 2026-08-11
Last-Modified 2026-09-08
SPDX-FileCopyrightText 2026-present Arthit Suriyawongkul
SPDX-FileType DOCUMENTATION
SPDX-License-Identifier CC0-1.0

Command line

Use this when you want a one-off SBOM from a terminal, a Makefile target, or any shell script. The console script is installed under two names, loom and pitloom -- pick whichever reads better; they run the same tool.

Quick guide

pip install pitloom
loom project .     # SBOM for the Python project in the current dir

loom -h shows the full option list.

Installation

pip install pitloom

Install with AI model metadata extraction support:

pip install "pitloom[ai]"

Install with extra content type detection:

pip install "pitloom[content-type]"

Install with SPDX 3 schema/SHACL validation support (pitloom fragment validate, pitloom validate-wheel):

pip install "pitloom[validate]"

Usage details

Generate an SBOM

Generate a Source SBOM for a Python project in the current directory:

loom project .
loom project /path/to/project -o sbom.spdx3.json

Limitation: the per-file inventory (file list, hashes, Merkle root) is backend-aware and accurate for Hatchling, setuptools, Poetry, PDM-backend, and Flit-core. Any other backend (uv_build, etc.) falls back to a Hatchling-based heuristic and logs a WARNING: -- the file list can be silently incomplete or mis-pathed. Project-level metadata (name, version, dependencies, license, authors) is read independently and unaffected either way.

If a lock file (pylock.toml, uv.lock, poetry.lock, pdm.lock, Pipfile.lock, or a fully pinned requirements.txt) is present next to pyproject.toml (or setup.py, for Pipfile.lock/requirements.txt), its resolved transitive dependencies are added to the Source SBOM's dependency list too -- see Dependency sources and precedence for which one wins when more than one is present, and what counts as "resolved" for each.

Generate an Analyzed SBOM from a pre-built wheel (extracting bundled binaries as phantom dependencies):

loom wheel path/to/mypackage-1.0.0-py3-none-any.whl -o sbom.spdx3.json

Embed an SBOM into a wheel (PEP 770)

Generate and embed an SPDX 3 SBOM directly into one or more built .whl files (writing to .dist-info/sboms/ and updating .dist-info/RECORD):

loom embed-wheel dist/mypackage-1.0.0-py3-none-any.whl
loom embed-wheel dist/*.whl --project-dir .

With --project-dir, the file list and hashes always come from the wheel itself, so they're accurate regardless of build backend. What can still be affected by the Source SBOM limitation above is --content-type and --extract-file-header, for any backend still on the Hatchling-based fallback (see above): that per-file enrichment can silently fail to attach to any file (falls back to no content-type/header data for it, not a wrong one).

Or inject an existing pre-generated SBOM into built wheels:

loom embed-wheel dist/*.whl --sbom sbom.spdx3.json

sbom.spdx3.json's declared subject name/version (PEP 503/440-normalised) is cross-checked against the target wheel's own .dist-info/METADATA before anything is written: a mismatch is an ERROR: that aborts the embed (exit 1, nothing written); pass --allow-mismatch to downgrade it to a WARNING: and embed anyway (useful for CI/automation that wants best-effort embedding). A Pitloom-generated SBOM (no --sbom) is never checked -- it's built from the same wheel metadata, so it can't diverge.

--sbom-basename NAME overrides the embedded file's basename (default: derived from the wheel's own name/version, <name>-<version>.spdx3.json). -o/--output names the modified wheel's own output path and is rejected with an ERROR: when more than one wheel is passed -- ambiguous without a per-wheel naming scheme; omit it to modify each wheel in place.

Check a wheel's embedded SBOM is at the correct PEP 770 location (.dist-info/sboms/), uses its format's recommended extension, and its declared subject name/version (PEP 503/440-normalised) match the wheel's own .dist-info/METADATA:

loom verify-wheel dist/*.whl
loom verify-wheel dist/mypackage-1.0.0-py3-none-any.whl --sbom-filename mypackage-1.0.0.spdx3.json
loom verify-wheel dist/*.whl --fail-on-mismatch

A missing SBOM is an ERROR: (exit 1); a present-but-non-conventional extension is a WARNING: only -- not fatal, still exit 0. Multiple sboms/ entries need --sbom-filename to pick one, else it's an ERROR:. A name/version mismatch is a WARNING: by default (exit 0); pass --fail-on-mismatch to make it an ERROR: (exit 1) instead. When the SBOM's subject name/version can't be extracted at all (unsupported format, or SPDX3 with an unexpected graph shape), the cross-check is skipped with a WARNING: naming why, regardless of --fail-on-mismatch.

Validate a wheel's embedded SBOM content against its format's schema and SHACL rules (currently SPDX3 JSON-LD only, via the same spdx3-validate library used by pitloom fragment validate -- needs pip install "pitloom[validate]"):

loom validate-wheel dist/*.whl

An embedded file in an unrecognised format prints a WARNING: and skips validation (exit 0) rather than failing -- unsupported isn't the same as invalid. embed-wheel itself takes --verify/--validate as convenience flags that run these same checks against the wheel just embedded:

loom embed-wheel dist/*.whl --project-dir . --verify --validate

Embedding and the post-embed check are independent steps -- a --verify/ --validate failure is reported and affects the exit code, but the embed itself isn't rolled back.

Or use --embed directly on loom wheel:

loom wheel dist/mypackage-1.0.0-py3-none-any.whl --embed

Generate a Deployed SBOM reflecting the exact installed environment graph:

loom env -o env.spdx3.json

Generate an Analyzed SBOM for a single AI model file, without a Python project directory. Supported local formats: GGUF, ONNX, Safetensors, PyTorch (.pt/.pth), Keras, HDF5, NumPy, fastText -- see AI model formats for the full extension/install-extra table:

loom model path/to/model.safetensors -o model.spdx3.json
loom model path/to/model.gguf --pretty

Or pass a Hugging Face Hub URL or model ID directly -- no local file required (needs pip install pitloom[huggingface_hub]):

loom model https://huggingface.co/mistralai/Mistral-7B-v0.1
loom model Qwen/Qwen3-235B-A22B   # bare model ID also works

Or use the smart unified entrypoint, which auto-detects the target type:

loom generate . -o sbom.spdx3.json                           # project directory -> Source SBOM
loom generate path/to/model.safetensors -o model.spdx3.json  # AI model asset    -> Analyzed SBOM
loom generate env -o env.spdx3.json                          # installed venv    -> Deployed SBOM

-o/--output is required for generate: unlike project/wheel/ model/env, which each know their target type and so have an obvious default filename, generate dispatches across several target types with no single natural default -- pass -o explicitly, or use the target-specific command for its own default.

Enrich an SBOM

Fill AI-model metadata gaps (license, datasets) from a local README.md/MODEL_CARD.md's YAML frontmatter -- off by default, opt in with --enrich on loom model/loom project/loom generate, or run it standalone to produce a mergeable fragment:

loom model path/to/model.safetensors --enrich -o model.spdx3.json

# Standalone: writes a fragment, doesn't generate a full SBOM
loom enrich path/to/model.safetensors -o model.enrich.spdx3.json
# When merging into a project-level (not single-model) base SBOM, add:
loom enrich path/to/model.safetensors --project-dir . -o model.enrich.spdx3.json

Register the fragment under [tool.pitloom.fragment] and re-run loom project/loom generate to merge it in.

Note: --project-dir's document identity (and every spdxId in the resulting SBOM) is derived from the resolved file list, so it changes whenever that file list changes for the same project -- e.g. after a Pitloom upgrade that changes file discovery for the project's build backend (see the Source SBOM limitation above). If merging into a base SBOM generated by an older Pitloom version, regenerate that base SBOM first -- otherwise the fragment's element references won't match the base document's ids, and the merge fails outright (see below).

For prose-reading enrichment (an AI agent reading the actual README text, not just its frontmatter), see the Agent Skills page instead -- the sbom-enrich skill.

Merge fragments

loom merge .spdx3-fragments/ -o combined.spdx3.json

Exits non-zero (with an ERROR: line, after a WARNING: naming each offending reference) if any element in the merged result references an id absent from the merge -- most commonly a fragment merged against a stale base SBOM (see the note above). Regenerate the base SBOM and re-run the fragment-producing step before merging again.

merge, fragment, and ids each take only their own small flag set, not the common options below -- e.g. --offline/-v/--registry/ --enrich don't apply to any of them. merge's own --pretty also defaults to True (pretty-printed), the opposite of every other subcommand's compact default.

Validate fragments

pitloom fragment validate combined.spdx3.json
pitloom fragment validate base.spdx3.json fragment.spdx3.json  # + merged-graph check

Checks JSON Schema and SHACL conformance via spdx3-validate's library API (requires the validate extra above). Works on any SPDX 3 JSON document, not just Pitloom's own output. Passing more than one path also validates the graph formed by merging them, which catches type errors across ExternalMap references -- pass --no-merge to skip that and check each document only in isolation. Non-zero exit reports every finding to stderr with every line ERROR:-tagged -- a SHACL violation's Severity/Source Shape/Focus Node breakdown spans several ERROR: lines, not just one.

Pin ids across fragments

Fragments are written by independent runs, so the same dataset or model would normally get a different spdxId in each run. Pin ids ahead of time, or reuse ids already present in an SBOM:

pitloom ids generate data src --entity model      # pin ids before running
pitloom ids import existing-sbom.spdx3.json       # or reuse ids from an SBOM

ids generate [PATH...] flags: -o/--registry FILE (registry file to update, default .pitloom-ids.json under --project-dir), --project-dir DIR, -e/--entity NAME[:TYPE] (repeatable -- register an explicit entity id ahead of a run; TYPE defaults to ai_AIPackage). ids import SBOM_FILE takes only -o/--registry FILE.

project/wheel/env also auto-harvest newly-minted ids back into the resolved registry after each run (--update-registry/--no-update-registry, on by default) -- see Loom IDs across fragments for what's excluded (ai_AIPackage, dataset_DatasetPackage) and why.

Useful flags

Available on project/generate/model/wheel/embed-wheel/env (not merge/fragment/ids, see above), unless noted otherwise:

  • -o FILE / --output FILE -- explicit output path.
  • --pretty -- indent the JSON for human reading (default: compact).
  • --offline -- forbid network access (PyPI/Hugging Face lookups). Not on enrich either.
  • -v / --verbose -- print effective options and where each came from.
  • --registry FILE -- Loom ID registry file path, overriding the auto-resolved default -- see Pin ids across fragments.
  • --describe-relationship / --no-describe-relationship -- include (or suppress) human-readable text on SPDX relationships.
  • --content-type / --no-content-type -- detect each file's real content type via magika/mimetypes (off by default: real per-file cost). --content-type-method {auto,magika,extension} picks the detector: auto tries magika and falls back to an extension guess, magika errors immediately if the magika package isn't installed, extension skips magika entirely (stdlib-only).

See Enrich an SBOM above for --enrich/--no-enrich.

Every subcommand that writes an SBOM (project, model, env, wheel, embed-wheel) prints PITLOOM_SBOM_OUTPUT_PATH=<path> to stdout after writing it -- the resolved path, including when a command's own default-naming logic picked it rather than an explicit -o. Scripts and CI can parse this line instead of re-deriving the default-naming logic themselves.

Debugging

--debug is global -- unlike the flags above, it works before any subcommand, including merge/fragment/ids:

loom --debug project .

Surfaces DEBUG:-level diagnostics on stderr (e.g. why a metadata extraction step was skipped) that are otherwise suppressed. Setting the PITLOOM_DEBUG environment variable (1/true/yes/on, case-insensitive) has the same effect and also covers entry points that don't parse this flag themselves: the Hatchling build hook and every public library-API function (generate_project_sbom(), etc.).

--no-debug overrides an ambient PITLOOM_DEBUG=1 back off for this invocation -- useful when it's set globally (a shell profile, CI) and a specific invocation should stay quiet. Omitting --debug entirely (neither flag given) leaves PITLOOM_DEBUG as found, ambient or not. Under the hood, --no-debug sets PITLOOM_DEBUG=0 in the process environment for the rest of the run; this only looks scoped to "one run" because the CLI process exits afterward. A script embedding Pitloom's library API and calling it more than once in one long-lived process should not rely on --no-debug/apply_debug_override(False) to reset itself between calls -- see apply_debug_override()'s docstring in pitloom/logging_config.py.

Configuration

See Configuration for the full reference -- every [tool.pitloom] setting, its default, and its CLI/Action/API mapping. The sections below walk through the two settings with the most nuance.

Creator and creation metadata

These flags apply to project, AI model, and Hugging Face SBOM generation alike. --creator-name is repeatable -- each occurrence starts a new creator, in order; --creator-type (person default, organization, software-agent, agent) and --creator-email set the type/email of the most recently named creator. --creation-tool records what produced it (default "Pitloom", also repeatable; --no-creation-tool to omit); --creation-comment/--creation-datetime set free-text provenance and an ISO 8601 timestamp:

loom project . --creator-name "Alice" --creator-email "alice@example.com"
loom project . --creator-name "Acme Corp" --creator-type organization
loom project . --creation-datetime "2026-01-15T10:00:00Z" --creation-comment "CI run #123"

The same fields can be set in pyproject.toml under [[tool.pitloom.creator]] / [[tool.pitloom.creation-tool]] (CLI flags take precedence, replacing the whole list rather than merging):

[[tool.pitloom.creator]]
name = "Alice"
email = "alice@example.com"
type = "person"       # or "organization", "software-agent", "agent"

[[tool.pitloom.creation-tool]]
name = "MyCompany SBOM Wrapper"

[tool.pitloom.creation]
creation-datetime = "2026-01-15T10:00:00Z"
creation-comment = "Generated in CI pipeline #123"

See Creation metadata for what these fields record and why.

Metadata provenance

Controlled by [tool.pitloom.provenance] in pyproject.toml:

[tool.pitloom.provenance]
format = "both"                    # "annotation" | "comment" | "both" (default)
detail = "minimal"                 # "minimal" (default) | "full"
preserve-source-metadata = "auto"  # "auto" (default) | "always" | "never"
max-source-metadata-bytes = 0      # 0 (default, unlimited) | a byte budget

max-source-metadata-bytes also has a --max-source-metadata-bytes BYTES CLI flag -- an operational override for the byte cap without editing pyproject.toml, unlike every other key above.

See Metadata provenance for what each setting does and worked examples.

See also