From c6aa45bbc5658725d486997e111a97c5effc8f62 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 15 Jun 2026 09:40:28 +0200 Subject: [PATCH 1/4] 23 separate rustc for build stage (#1) * Separated builder-stage rust image from final-stage using cli_rust_version * Added cli_rust_image_digest to publish matrix and workflow * Defaulted CLI_RUST_IMAGE_DIGEST to RUST_IMAGE_DIGEST when unset --- .github/workflows/publish.yml | 1 + Dockerfile | 7 ++++++- builds.json | 3 +++ builds.schema.json | 6 +++++- scripts/build_image.py | 6 ++++++ scripts/resolve_matrix.py | 3 +++ tests/unit/test_resolve_matrix.py | 1 + 7 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5996a76..12618a7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -109,6 +109,7 @@ jobs: RUST_VERSION=${{ matrix.rust_version }} RUST_BASE_SUFFIX=${{ matrix.rust_base_suffix }} RUST_IMAGE_DIGEST=${{ matrix.rust_image_digest }} + CLI_RUST_IMAGE_DIGEST=${{ matrix.cli_rust_image_digest }} STELLAR_CLI_REV=${{ matrix.stellar_cli_ref }} STELLAR_CLI_VERSION=${{ matrix.stellar_cli_version }} BUILD_DATE=${{ steps.meta.outputs.build_date }} diff --git a/Dockerfile b/Dockerfile index 5aa1320..2b45554 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,12 +15,17 @@ ARG RUST_VERSION ARG RUST_BASE_SUFFIX ARG RUST_IMAGE_DIGEST +# Falls back to RUST_IMAGE_DIGEST so direct `docker build` usage keeps working +# without supplying CLI_RUST_IMAGE_DIGEST. The build scripts and CI always set +# it explicitly (from cli_rust_version) so the builder stage can diverge from +# the final stage's base image. +ARG CLI_RUST_IMAGE_DIGEST=${RUST_IMAGE_DIGEST} ARG STELLAR_CLI_REV ARG STELLAR_CLI_VERSION ARG BUILD_DATE ARG SOURCE_REPO -FROM rust@${RUST_IMAGE_DIGEST} AS builder +FROM rust@${CLI_RUST_IMAGE_DIGEST} AS builder ARG STELLAR_CLI_REV ARG STELLAR_CLI_VERSION SHELL ["/bin/bash", "-eo", "pipefail", "-c"] diff --git a/builds.json b/builds.json index b3ff10e..dc01156 100644 --- a/builds.json +++ b/builds.json @@ -4,6 +4,7 @@ "default_distro": "trixie", "stellar_cli_versions": [ { + "cli_rust_version": "1.90.0-slim-bookworm@sha256:64232e656c058f4468e8d024e990acff04f0fd5a5c0a88a574dc37773d7325c9", "ref": "a048a57a75762458b487052e0021ea704a926bee", "rust_versions": [ "1.90.0-slim-bookworm@sha256:64232e656c058f4468e8d024e990acff04f0fd5a5c0a88a574dc37773d7325c9" @@ -11,6 +12,7 @@ "version": "25.1.0" }, { + "cli_rust_version": "1.95.0-slim-trixie@sha256:e14e87345b4d5964ddcc3491d27ee046a0f23820f340c3c1e24da6880141f7c0", "ref": "60f7458e7ecffddf2f2d91dc6d0d2db4fab03ecc", "rust_versions": [ "1.93.0-slim-trixie@sha256:760ad1d638d70ebbd0c61e06210e1289cbe45ff6425e3ea6e01241de3e14d08e", @@ -19,6 +21,7 @@ "version": "26.0.0" }, { + "cli_rust_version": "1.95.0-slim-trixie@sha256:e14e87345b4d5964ddcc3491d27ee046a0f23820f340c3c1e24da6880141f7c0", "ref": "1228cff8022b804659750b94b315932b0e0f3f6a", "rust_versions": [ "1.95.0-slim-trixie@sha256:e14e87345b4d5964ddcc3491d27ee046a0f23820f340c3c1e24da6880141f7c0" diff --git a/builds.schema.json b/builds.schema.json index 05b116c..2b7585d 100644 --- a/builds.schema.json +++ b/builds.schema.json @@ -40,12 +40,16 @@ "items": { "additionalProperties": false, "properties": { + "cli_rust_version": { + "$ref": "#/definitions/rust_pin", + "description": "Fully-qualified rust base pin (@) to build this stellar-cli release - not the same as the rustc used to compile contracts" + }, "ref": { "$ref": "#/definitions/git_sha", "description": "Upstream stellar/stellar-cli git commit SHA for this release. Source of truth; version is metadata." }, "rust_versions": { - "description": "Append-only list of fully-qualified rust base pins (@) 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) — each pin is its own immutable image.", + "description": "Append-only list of fully-qualified rust base pins (@) 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" }, diff --git a/scripts/build_image.py b/scripts/build_image.py index 5057a46..27d3636 100755 --- a/scripts/build_image.py +++ b/scripts/build_image.py @@ -36,6 +36,9 @@ def main(argv: list[str] | None = None) -> int: ) stellar_ref = builds.stellar_cli_ref(data, args.stellar_cli_version) parsed = rust_keys.parse(args.rust_version) + entry = builds.find_cli(data, args.stellar_cli_version) + cli_rust_pin = entry.get("cli_rust_version") if entry else None + cli_rust_digest = builds.digest_of(cli_rust_pin) if cli_rust_pin else rust_digest except ValueError as exc: common.die(str(exc)) @@ -48,6 +51,7 @@ def main(argv: list[str] | None = None) -> int: common.log(f"building {tag}") common.log(f" stellar-cli {args.stellar_cli_version} ({stellar_ref})") common.log(f" rust {args.rust_version} ({rust_digest})") + common.log(f" cli-build rust ({cli_rust_digest})") common.log(f" base rust:{parsed.version}-{parsed.suffix}") common.log(f" platform {args.platform or ''}") @@ -63,6 +67,8 @@ def main(argv: list[str] | None = None) -> int: "--build-arg", f"RUST_IMAGE_DIGEST={rust_digest}", "--build-arg", + f"CLI_RUST_IMAGE_DIGEST={cli_rust_digest}", + "--build-arg", f"STELLAR_CLI_REV={stellar_ref}", "--build-arg", f"STELLAR_CLI_VERSION={args.stellar_cli_version}", diff --git a/scripts/resolve_matrix.py b/scripts/resolve_matrix.py index a980d48..df373ff 100755 --- a/scripts/resolve_matrix.py +++ b/scripts/resolve_matrix.py @@ -26,6 +26,8 @@ def build_matrix(data: dict, only_cli: str = "") -> dict: if only_cli and cli != only_cli: continue ref = entry["ref"] + cli_rust_pin = entry.get("cli_rust_version") + cli_rust_image_digest = builds.digest_of(cli_rust_pin) if cli_rust_pin else None # The published tag is label-only, so only the newest pin per label is # built — the last occurrence in rust_versions wins. builds.json keeps # the superseded pins as history; they are not rebuilt or republished. @@ -38,6 +40,7 @@ def build_matrix(data: dict, only_cli: str = "") -> dict: rows.append( { "arch": arch, + "cli_rust_image_digest": cli_rust_image_digest or digest, "platform": f"linux/{arch}", "rust_base_id": rust_base_id, "rust_base_key": label, diff --git a/tests/unit/test_resolve_matrix.py b/tests/unit/test_resolve_matrix.py index f79055f..51e656e 100644 --- a/tests/unit/test_resolve_matrix.py +++ b/tests/unit/test_resolve_matrix.py @@ -28,6 +28,7 @@ def test_build_matrix_row_carries_expected_keys(minimal_builds: dict) -> None: row = matrix["include"][0] assert set(row.keys()) == { "arch", + "cli_rust_image_digest", "platform", "rust_base_id", "rust_base_key", From f78be5a746ebd433e129664338877d0fd5d674fe Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 15 Jun 2026 09:43:13 +0200 Subject: [PATCH 2/4] Simpler cli flags (#2) * Made --rust-image-digest optional when unambiguous * Addressed PR review comments on rust digest resolution * Update CI to make --rust-image-digest optional --- .github/workflows/build.yml | 2 -- README.md | 8 +++-- RELEASE.md | 8 ++--- scripts/build_image.py | 7 ++-- scripts/lib/builds.py | 29 +++++++++++++++++ tests/unit/test_build_image.py | 59 ++++++++++++++++++++++++++++++++++ tests/unit/test_builds.py | 43 +++++++++++++++++++++++++ 7 files changed, 143 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ff1a1f..d54c2b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,14 +39,12 @@ jobs: ./scripts/build_image.py \ --stellar-cli-version "${{ steps.pair.outputs.cli }}" \ --rust-version "${{ steps.pair.outputs.rust }}" \ - --rust-image-digest "${{ steps.pair.outputs.digest }}" - name: smoke test run: | ./scripts/smoke_test_image.py \ --image "${{ steps.pair.outputs.image }}" \ --stellar-cli-version "${{ steps.pair.outputs.cli }}" \ --rust-version "${{ steps.pair.outputs.rust }}" \ - --rust-image-digest "${{ steps.pair.outputs.digest }}" - name: wasm reproducibility run: | ./scripts/repro_test.py \ diff --git a/README.md b/README.md index b29c5b4..4dc20e6 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,12 @@ compare the resulting WASM sha256. ./scripts/validate_json.py # Build a local image for a declared (cli, rust base) pair. The rust base is -# given as the label plus its pinned digest (copy the pin from builds.json). +# given as the label; the digest is resolved from builds.json automatically. ./scripts/build_image.py --stellar-cli-version 26.0.0 \ - --rust-version 1.94.0-slim-trixie \ - --rust-image-digest sha256:f7bf1c266d9e48c8d724733fd97ba60464c44b743eb4f46f935577d3242d81d0 + --rust-version 1.94.0-slim-trixie + +# Only when a label carries more than one digest in builds.json do you need +# --rust-image-digest to say which pin to build. # Smoke-test the built image. docker run --rm stellar-cli:26.0.0-rust1.94.0-slim-trixie --version diff --git a/RELEASE.md b/RELEASE.md index a3a5890..7764bc6 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -102,11 +102,11 @@ The script prints the chosen release tag as its final stdout line. Commit and pu ```sh ./scripts/validate_json.py -# --rust-version is the label; --rust-image-digest is the pin's digest from -# builds.json (it pins the FROM and is cross-checked against the image's +# --rust-version is the label; build_image.py resolves its pinned digest from +# builds.json (the digest pins the FROM and is cross-checked against the image's # org.opencontainers.image.base.digest label). The built tag is label-only. -./scripts/build_image.py --stellar-cli-version 26.1.0 --rust-version 1.95.0-slim-trixie \ - --rust-image-digest sha256:e14e87345b4d5964ddcc3491d27ee046a0f23820f340c3c1e24da6880141f7c0 +# Pass --rust-image-digest only when one label carries multiple digests. +./scripts/build_image.py --stellar-cli-version 26.1.0 --rust-version 1.95.0-slim-trixie ./scripts/smoke_test_image.py --image stellar-cli:26.1.0-rust1.95.0-slim-trixie \ --stellar-cli-version 26.1.0 --rust-version 1.95.0-slim-trixie \ --rust-image-digest sha256:e14e87345b4d5964ddcc3491d27ee046a0f23820f340c3c1e24da6880141f7c0 diff --git a/scripts/build_image.py b/scripts/build_image.py index 27d3636..74bebbe 100755 --- a/scripts/build_image.py +++ b/scripts/build_image.py @@ -17,7 +17,7 @@ def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--stellar-cli-version", required=True, metavar="V") parser.add_argument("--rust-version", required=True, metavar="KEY") - parser.add_argument("--rust-image-digest", required=True, metavar="DIGEST") + parser.add_argument("--rust-image-digest", default=None, metavar="DIGEST") parser.add_argument("--platform", default="", metavar="P") parser.add_argument("--tag", default="", metavar="REF") parser.add_argument("--source-repo", default="stellar/stellar-cli-docker", metavar="SLUG") @@ -29,10 +29,9 @@ def main(argv: list[str] | None = None) -> int: common.preflight_checks(["buildx"]) data = builds.load() - rust_digest = args.rust_image_digest try: - builds.assert_pair_declared( - data, args.stellar_cli_version, f"{args.rust_version}@{rust_digest}" + rust_digest = builds.resolve_rust_digest( + data, args.stellar_cli_version, args.rust_version, args.rust_image_digest ) stellar_ref = builds.stellar_cli_ref(data, args.stellar_cli_version) parsed = rust_keys.parse(args.rust_version) diff --git a/scripts/lib/builds.py b/scripts/lib/builds.py index 3d169a6..4ff0d76 100644 --- a/scripts/lib/builds.py +++ b/scripts/lib/builds.py @@ -81,6 +81,35 @@ def assert_pair_declared(data: dict[str, Any], cli: str, rust_pin: str) -> None: ) +def resolve_rust_digest( + data: dict[str, Any], cli: str, label: str, digest: str | None = None +) -> str: + """Resolve the pinned base digest for a (cli, rust label) pair. + + When `digest` is given, assert the exact `