1
0
Fork 0
mirror of https://github.com/imjasonh/terraform-playground synced 2026-07-16 20:54:50 +00:00
terraform-playground/pymage
Cursor Agent c15f3e350c
cli: build multi-arch image index from multiple --platform values
- --platform is now repeatable/comma-separated; >1 platform builds one image
  per platform and assembles them into an OCI image index (remote.WriteIndex /
  OCI layout). SBOM aggregates wheels across platforms.
- adds TestBuildMultiArchIndex (2-platform index, reproducible) against an
  in-process registry; documents multi-arch in the README

Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
2026-06-10 15:45:36 +00:00
..
e2e Rename py-image-builder to pymage (module, imports, CLI, workflow, docs) 2026-06-10 13:30:05 +00:00
internal cli: build multi-arch image index from multiple --platform values 2026-06-10 15:45:36 +00:00
DESIGN.md build/cli: layer cache + parallel wheel layers, secret deny-list, env validation, docs 2026-06-10 14:07:25 +00:00
go.mod Rename py-image-builder to pymage (module, imports, CLI, workflow, docs) 2026-06-10 13:30:05 +00:00
go.sum Rename py-image-builder to pymage (module, imports, CLI, workflow, docs) 2026-06-10 13:30:05 +00:00
main.go Rename py-image-builder to pymage (module, imports, CLI, workflow, docs) 2026-06-10 13:30:05 +00:00
README.md cli: build multi-arch image index from multiple --platform values 2026-06-10 15:45:36 +00:00

pymage

A docker-less, layer-aware container image builder for Python applications, in the spirit of ko. It builds and pushes OCI images without a Docker daemon by composing content-addressed layers with go-containerregistry.

See DESIGN.md for the full rationale.

What makes it efficient

  • One deterministic layer per wheel. Installing a wheel produces a byte-identical layer every time, so its digest is stable.
  • No-bytes rebuilds. Because layers are content-addressed and the builder is reproducible, re-pushing an unchanged image transfers zero dependency bytes — the registry already has every blob (verified by a HEAD check).
  • Only new dependencies upload. Adding a dependency creates exactly one new layer; every existing dependency layer keeps its digest and is skipped.
  • App code is a thin top layer, so the common edit-rebuild loop only moves a small layer (and the manifest).
  • Reproducible: same lock + same source + same base ⇒ same image digest.

Usage

# --lock is a pinned + hashed requirements file
#   (pip-compile / uv pip compile --generate-hashes)
# --find-links is a directory of the resolved .whl files
# --source is the application source (optional)
pymage build \
  --base cgr.dev/chainguard/python:latest \
  --lock requirements.txt \
  --find-links ./wheelhouse \
  --source ./ \
  --entrypoint python --entrypoint -m --entrypoint myapp \
  -t registry.example.com/me/myapp:latest

The wheels referenced by the lock must be present in a --find-links directory (e.g. produced by pip download -r requirements.txt -d ./wheelhouse). Resolution is intentionally local so builds are hermetic and reproducible.

Multi-arch

Pass multiple platforms to build a multi-arch image index (one image per platform, assembled into an OCI index). Because no Docker daemon is involved, this works from any host OS — Linux, macOS, or Windows:

pymage build \
  --base python:3.12-slim \
  --platform linux/amd64,linux/arm64 \
  --lock requirements.txt --find-links ./wheelhouse --source ./ \
  --entrypoint python --entrypoint -m --entrypoint myapp \
  -t registry.example.com/me/myapp:latest

Each platform selects its own compatible wheels (pure-python wheels are shared), so the wheelhouse must contain a compatible wheel per platform for any compiled dependency.

Useful flags

Flag Description
--push=false Build without pushing (combine with --oci-layout).
--oci-layout DIR Also write the image to an OCI layout directory.
--print-digest Print only the resulting image digest (no push).
--sbom PATH Write a CycloneDX SBOM of the resolved wheels.
--layer-strategy per-wheel (default) or single-deps-layer.
--platform Target platform(s); selects compatible wheels and base. Repeatable / comma-separated (e.g. linux/amd64,linux/arm64) builds a multi-arch image index.
--python Interpreter version / site-packages dir (default python3.12); also selects compatible wheels.
--cache-dir Content-addressed layer cache; reuses compressed layers across rebuilds.
--prefix install prefix / venv root (default /app/.venv).
--workdir image working dir and source destination (default /app).
--user image user, e.g. 65532.
--insecure use plain HTTP for the registry.
--require-hashes require --hash on every requirement (default true).

Layout

Package Responsibility
internal/ptar Deterministic tar + OCI layer construction.
internal/wheel Parse a wheel and lay it out into installed files.
internal/lock Parse hashed requirements.txt.
internal/wheelhouse Resolve requirements to local wheel files (hash-checked).
internal/build Assemble base + per-wheel layers + app layer; rewrite config.
internal/sbom Emit a deterministic CycloneDX SBOM.
internal/cli The build command.
e2e End-to-end tests against a local registry.

Testing

go test ./...

Tests are hermetic (no network, no Docker): wheels are synthesized in-process and pushes/pulls go to go-containerregistry's in-process registry served over HTTP. The e2e package demonstrates:

  • reproducibility — two independent builds yield the same image digest;
  • no-bytes rebuild — re-pushing an unchanged image uploads zero blobs, and adding one dependency uploads exactly one new dependency layer;
  • correctness — the installed packages import under a real python3.