Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions cargo/Dockerfile.cargo
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Dockerfile.cargo — reproducible-build image for wasms built with PLAIN `cargo build`
# (the ~2,600 on-chain wasms with rsver/rssdkver but NO cliver were built without the
# stellar cli; the SDK macros still emit contractspecv0/contractenvmetav0 + rsver/
# rssdkver, but no cliver and no wasm-opt).
#
# SEP-0058 use: this image IS the `bldimg`; `bldopt` is the FULL argv appended to the
# entrypoint (so a verifier just runs `docker run -v src:/source <bldimg> <bldopt>`).
# The entrypoint is the bare tool `cargo`, so `bldopt` leads with the `build`
# subcommand — mirroring the stellar path, whose bldimg entrypoint is bare `stellar`
# and whose bldopt leads with `contract build`. Verifier flow:
# docker run -v <src>:/source <bldimg> build --release --target=<t> -p <pkg> --manifest-path <path>
# Declared before FROM so it is in scope for the pinned base reference; the
# build stage re-declares it below to use it again in the LABELs.
ARG RUST_IMAGE_DIGEST
FROM rust@${RUST_IMAGE_DIGEST}
SHELL ["/bin/bash", "-eo", "pipefail", "-c"]
ARG RUST_VERSION
ARG RUST_BASE_SUFFIX
ARG RUST_IMAGE_DIGEST
ARG BUILD_DATE
ARG SOURCE_REPO

# RUSTUP_TOOLCHAIN is baked in so an in-source `rust-toolchain.toml` in a
# consumer's contract can't silently swap our pinned toolchain at build
# time. Required by SEP-58 "self-contained build environment". Consumers can
# still override with `-e RUSTUP_TOOLCHAIN=...` if they know what they're
# doing.
ENV DEBIAN_FRONTEND=noninteractive \
RUSTUP_TOOLCHAIN=${RUST_VERSION}

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
libdbus-1-3 \
libssl3 \
libudev1 \
&& rm -rf /var/lib/apt/lists/*
RUN rustup target add wasm32-unknown-unknown \
&& (rustup target add wasm32v1-none || true)

RUN useradd --create-home --home-dir /stellar --uid 1000 --shell /bin/bash stellar \
&& mkdir -p /source /config /data \
&& chown stellar:stellar /source /config /data

ENV CARGO_HOME=/stellar/.cargo \
HOME=/stellar
USER stellar
WORKDIR /source

ENTRYPOINT ["cargo"]
CMD []

LABEL org.opencontainers.image.title="cargo" \
org.opencontainers.image.description="Cargo build image (SEP-58-compatible image for Stellar smart contracts)." \
org.opencontainers.image.source="https://github.com/${SOURCE_REPO}" \
org.opencontainers.image.url="https://github.com/${SOURCE_REPO}" \
org.opencontainers.image.documentation="https://github.com/${SOURCE_REPO}" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.base.name="docker.io/library/rust:${RUST_VERSION}-${RUST_BASE_SUFFIX}" \
org.opencontainers.image.base.digest="${RUST_IMAGE_DIGEST}"
166 changes: 166 additions & 0 deletions cargo/build_all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env -S uv run python
"""Build and publish the cargo build image for every rust version in builds_rust.json.

This is the cargo analogue of scripts/build_all.py, but radically simpler:
there is no (cli x rust) matrix here, just a flat list of rust base pins. For
each pin we build cargo/Dockerfile.cargo on top of the pinned `rust:<label>`
base and push it to `<registry>:<rust_version>` (e.g. docker.io/vrfier/rust:1.94.0).

The image is a thin customization of the upstream rust base (apt deps + wasm
targets + a non-root user), so each build is fast and mostly cache hits.

Each builds_rust.json entry is a pin `<label>@<digest>`, e.g.
`1.94.0-slim-trixie@sha256:...`. We split it into:
RUST_VERSION 1.94.0 -> also the published tag
RUST_BASE_SUFFIX slim-trixie
RUST_IMAGE_DIGEST sha256:... -> pins the exact FROM bytes

Pushing is outward-facing: run with --dry-run first to print the exact
commands, and make sure you are logged in to the registry (`docker login`)
before a real run.
"""

import argparse
import datetime
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
BUILDS_FILE = HERE / "builds_rust.json"
DOCKERFILE = HERE / "Dockerfile.cargo"

DEFAULT_REGISTRY = "docker.io/vrfier/rust"
DEFAULT_SOURCE_REPO = "vrfier/stellar-cli-docker"
DEFAULT_PLATFORM = "linux/amd64"

# Version is always three dotted ints; suffix is everything after the first dash.
_PIN = re.compile(r"^(?P<version>[0-9]+\.[0-9]+\.[0-9]+)-(?P<suffix>[^@]+)@(?P<digest>sha256:[0-9a-f]+)$")


def log(message: str) -> None:
print(message, file=sys.stderr)


def die(message: str) -> None:
print(f"error: {message}", file=sys.stderr)
sys.exit(1)


def parse_pin(pin: str) -> tuple[str, str, str]:
"""Split `<version>-<suffix>@<digest>` into (version, suffix, digest)."""
match = _PIN.match(pin.strip())
if match is None:
raise ValueError(f"invalid rust base pin: {pin!r} (expected <version>-<suffix>@sha256:...)")
return match["version"], match["suffix"], match["digest"]


def load_pins(only_version: str) -> list[str]:
data = json.loads(BUILDS_FILE.read_text())
pins = data.get("rust_versions", [])
if not only_version:
return pins
kept = [p for p in pins if parse_pin(p)[0] == only_version]
if not kept:
die(f"rust {only_version} is not declared in {BUILDS_FILE.name}")
return kept


def require_buildx() -> None:
if shutil.which("docker") is None:
die("docker is required (needed for buildx)")
if subprocess.run(["docker", "buildx", "version"], capture_output=True).returncode != 0:
die("docker buildx plugin is required; install it or upgrade docker")
if subprocess.run(["docker", "info"], capture_output=True).returncode != 0:
die("docker daemon is not reachable; start it (e.g. start Docker Desktop / OrbStack)")


def build_one(pin: str, args: argparse.Namespace, build_date: str) -> None:
version, suffix, digest = parse_pin(pin)
tag = f"{args.registry}:{version}"

cmd = ["docker", "buildx", "build", "-f", str(DOCKERFILE)]
if args.platform:
cmd += ["--platform", args.platform]
cmd += [
"--push" if args.push else "--load",
"--build-arg", f"RUST_VERSION={version}",
"--build-arg", f"RUST_BASE_SUFFIX={suffix}",
"--build-arg", f"RUST_IMAGE_DIGEST={digest}",
"--build-arg", f"BUILD_DATE={build_date}",
"--build-arg", f"SOURCE_REPO={args.source_repo}",
"--tag", tag,
str(HERE),
]

log(f"building {tag}")
log(f" base rust:{version}-{suffix} ({digest})")
log(f" platform {args.platform or '<host native>'}")
log(f" output {'push to registry' if args.push else 'load into local docker'}")
if args.dry_run:
log(" " + " ".join(cmd))
return
subprocess.run(cmd, check=True)
log(f"done: {tag}")


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--registry", default=DEFAULT_REGISTRY, metavar="REF")
parser.add_argument("--source-repo", default=DEFAULT_SOURCE_REPO, metavar="SLUG")
parser.add_argument(
"--rust-version",
default="",
metavar="V",
help="Limit to one rust version (default: every version in builds_rust.json).",
)
parser.add_argument(
"--platform",
default=DEFAULT_PLATFORM,
metavar="P",
help=(
"buildx --platform value. Default linux/amd64. Pass e.g. "
"linux/amd64,linux/arm64 for a multi-arch push (requires --push)."
),
)
parser.add_argument(
"--load",
dest="push",
action="store_false",
help="Load the image into the local docker instead of pushing it.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print every build command without running anything.",
)
parser.set_defaults(push=True)
return parser


def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if not args.dry_run:
require_buildx()

pins = load_pins(args.rust_version)
build_date = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ")

log(f"building {len(pins)} image(s) into {args.registry}")
log(f"source-repo {args.source_repo}")
if args.dry_run:
log("(dry run — no images will be built or pushed)")

for pin in pins:
build_one(pin, args, build_date)

log("")
log(f"done: {len(pins)} image(s) processed")
return 0


if __name__ == "__main__":
sys.exit(main())
22 changes: 22 additions & 0 deletions cargo/builds_rust.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$comment": "Source of truth for which rust versions we publish. Each entry is a fully-qualified rust base pin '<rust_base_key>@<index-digest>' (e.g. 1.94.0-slim-trixie@sha256:...)",
"$schema": "./builds_rust.schema.json",
"default_distro": "trixie",
"rust_versions": [
"1.85.0-slim-bookworm@sha256:1829c432be4a592f3021501334d3fcca24f238432b13306a4e62669dec538e52",
"1.86.0-slim-bookworm@sha256:57d415bbd61ce11e2d5f73de068103c7bd9f3188dc132c97cef4a8f62989e944",
"1.87.0-slim-bookworm@sha256:437507c3e719e4f968033b88d851ffa9f5aceeb2dcc2482cc6cb7647811a55eb",
"1.88.0-slim-trixie@sha256:9a7159329166b45f453351a077367f501aa3e98378f7e327530e7966a139d05f",
"1.89.0-slim-trixie@sha256:8cffb8fe4e8a95cf0d6a2060375e5a28aff4c752155aa9f1f9193530769bdf66",
"1.90.0-slim-trixie@sha256:7fa728f3678acf5980d5db70960cf8491aff9411976789086676bdf0c19db39e",
"1.91.0-slim-trixie@sha256:d9ba8014603166915f7e0fcaa9af09df2a1fc30547e75a72c1d34165139f036a",
"1.91.1-slim-trixie@sha256:f75071363e7f4771769d4cf81b1b7b290e607f4d4459e8731f6abdcee9982dc8",
"1.92.0-slim-trixie@sha256:bf3368a992915f128293ac76917ab6e561e4dda883273c8f5c9f6f8ea37a378e",
"1.93.0-slim-trixie@sha256:760ad1d638d70ebbd0c61e06210e1289cbe45ff6425e3ea6e01241de3e14d08e",
"1.93.1-slim-trixie@sha256:c0a38f5662afdb298898da1d70b909af4bda4e0acff2dc52aea6360a9b9c6956",
"1.94.0-slim-trixie@sha256:f7bf1c266d9e48c8d724733fd97ba60464c44b743eb4f46f935577d3242d81d0",
"1.94.1-slim-trixie@sha256:cf09adf8c3ebaba10779e5c23ff7fe4df4cccdab8a91f199b0c142c53fef3e1a",
"1.95.0-slim-trixie@sha256:e14e87345b4d5964ddcc3491d27ee046a0f23820f340c3c1e24da6880141f7c0",
"1.96.0-slim-trixie@sha256:26abcef3d79b8d890c4ceb17093154573e1f6479cf6dd7c1450043b8458350f6"
]
}
42 changes: 42 additions & 0 deletions cargo/builds_rust.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"definitions": {
"rust_pin": {
"description": "A fully-qualified rust base pin: the composite rust base key (rust toolchain version + upstream rust:<v>-<suffix> tail, e.g. 1.94.0-slim-trixie) joined to the pinned multi-arch index digest with '@'. Example: 1.94.0-slim-trixie@sha256:<64 hex>.",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+-[a-z][a-z0-9-]*@sha256:[0-9a-f]{64}$",
"type": "string"
}
},
"properties": {
"$comment": {
"type": "string"
},
"$schema": {
"type": "string"
},
"default_distro": {
"description": "Debian codename used to compose the upstream rust image suffix (slim-<codename>). Drives the picker in scripts/release-prepare.sh and the :<cli> / :latest alias derivation.",
"enum": [
"bookworm",
"trixie"
],
"type": "string"
},
"rust_versions": {
"description": "Append-only list of fully-qualified rust base pins (<rust_base_key>@<image_digest>) this stellar-cli release was ever built against. Refreshes append the picker's freshly-resolved pins; previously-published pins are retained so the file stays consistent with the immutable tags in the registry. The same label may appear more than once with distinct digests (e.g. a rebuilt base) \u2014 each pin is its own immutable image.",
"items": {
"$ref": "#/definitions/rust_pin"
},
"minItems": 1,
"type": "array",
"uniqueItems": true
}
},
"required": [
"rust_versions",
"default_distro"
],
"title": "stellar-cli-docker builds_rust.json",
"type": "object"
}