Oci
The OCI rules model image movement and deterministic image assembly without a
Docker daemon. ociPull() fetches an immutable layout, ociBuild() composes
file layers and metadata, ociPush() publishes a local image, and
ociMirror() copies directly between registries. A managed Crane toolchain
handles registry operations.
Set up Crane
Registry operations use the pinned Crane release provided by the OCI rule:
import "//rules/oci";
The release is acquired as an immutable graph artifact on first use. Pure local
composition of a "scratch" image does not invoke Crane, but pulls, publishes,
mirrors, and builds based on a pulled image do.
Select a workspace lockfile
The Crane release ships with a lockfile pinning the download URL, size, and SHA-256 of every artifact it knows. To pin a version the shipped lockfile does not know, give the toolchain the address of a lockfile this workspace owns; the shipped lockfile stays the default.
import { craneToolchain, craneGenLockfiles } from "//rules/oci/toolchain";
export const crane = craneToolchain("0.19.0", {
default: true,
lockfile: "//locks/crane.lock",
});
export const craneLockfiles = craneGenLockfiles();imp goal gen-lockfiles //:craneLockfiles
craneGenLockfiles() writes to the address the toolchain declares, so the
address is given one time only. Downloads stay verified: an address with no
file, or a lockfile with no entry for the selected version and platform, makes
the acquire fail and points at imp goal gen-lockfiles.
Pull a base and build an image
import { ociPull, ociBuild } from "//rules/oci";
export const alpine = ociPull({
repo: "docker.io/library/alpine",
digest: "sha256:...",
});
export const app = ociBuild({
base: alpine,
sourceBase: "images",
path: ".",
layers: [{
srcs: ["bin/server"],
path: "/usr/local/bin",
mode: "0755",
}],
entrypoint: ["/usr/local/bin/server"],
env: { LOG_LEVEL: "info" },
workdir: "/srv",
});
A pull requires exactly one of tag or digest. Tags are resolved on every
invocation because they can move; the resolved digest and image layout then
flow through immutable artifact handles. Prefer a digest when reproducibility
matters.
ociBuild accepts an ociPull/ociBuild image as its base or the literal
"scratch". It does not interpret a Dockerfile and cannot run RUN commands.
Each layer stages workspace files selected by srcs at the requested image
path, with optional exclusions, ownership, and mode. Layer tarballs, config,
manifest, and OCI index are assembled deterministically, so identical inputs
produce identical image content.
sourceBase defaults to the declaring package. Pass it explicitly through a
BUILD helper that constructs images for another package.
Build and package locally
imp build //images:app
imp package //images:app
build leaves the OCI layout in the build graph/CAS. package writes
dist/images/app/image.tar, an OCI archive suitable for tools such as
podman load or docker load. Pulled images can be packaged in the same way.
Push or mirror
import { ociPush, ociMirror } from "//rules/oci";
export const publish = ociPush({
image: app,
repo: "registry.example.com/acme/server",
tag: "latest",
});
export const mirror = ociMirror({
from: { repo: "docker.io/library/alpine", tag: "3.23" },
to: { repo: "registry.example.com/mirror/alpine", tag: "3.23" },
});
Select either target with imp publish. Push and mirror are intentionally
impure: they perform registry side effects on every invocation and are never
replayed from the task cache. Mirror uses a registry-to-registry Crane copy and
does not materialize the image locally.
Credential sourcing is not implemented yet, so these rules are presently suitable for public/anonymous registries only. Do not assume an ambient Docker login will be visible inside the hermetic execution environment.
The current image builder emits one image manifest. Multi-platform indexes and Dockerfile-compatible command execution are outside this rule's present surface.