From 65c93cf20b3fc3fb4a8771f9fe7b60bde1bc56cd Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 1 Sep 2026 01:13:25 +0500 Subject: [PATCH] fix(image): let rustup own the rust surface, and carry its channels to the builder Three coupled defects left b19 unbuildable and unverifiable: - The guest toolchain projection dropped `channels` and `default_channel`. The manifest named them, the manifest tests guarded them, and provision.sh still received a rustup entry with no channels, failing the build on a bare conditional with nothing on stderr. Both real builder attempts died exactly there, five minutes in. - Neither smoke script knew rustup in its toolchain sets, so a build that survived provisioning would have failed smoke on the set comparison. - The standalone rust 1.97.1 tarball promised a system toolchain the image could not keep: the rustup shims overwrite /usr/local/bin/rustc during provisioning, so the smoke expectation of `rustc 1.97.1` could never hold in the shipped image. rustup is now the single owner of the Rust surface: the manifests drop the standalone tarball, default channel 1.98.0 serves PATH callers, and the 1.89 MSRV channel rides beside it with clippy and rustfmt on both. provision.sh says what is missing instead of dying silently, both smoke scripts verify every pinned channel and the default through the shims, and the projection into the guest contract is one function with a test that fails when a field is dropped. The benchmark installer follows the image to 1.98.0. Claude-Session: https://claude.ai/code/session_01LsGid6U5RrQdFvJmvYdGCF --- .../golden-image-container-integration.yaml | 5 --- config/golden-image-container.yaml | 5 --- config/golden-image-integration.yaml | 5 --- config/golden-image.yaml | 5 --- internal/imagebuild/assets/provision.sh | 33 ++++----------- .../imagebuild/assets/smoke-integration.sh | 25 +++++++++-- internal/imagebuild/assets/smoke.sh | 29 ++++++++++--- internal/imagebuild/orchestrator.go | 35 +++++++++++----- internal/imagebuild/orchestrator_test.go | 41 ++++++++++++++++++- internal/imagemanifest/manifest_test.go | 3 +- internal/imagemanifest/types.go | 13 +++--- internal/imageplan/plan_test.go | 2 +- .../external_downloads_test.go | 2 +- scripts/install-benchmark-toolchain.sh | 10 ++--- 14 files changed, 130 insertions(+), 83 deletions(-) diff --git a/config/golden-image-container-integration.yaml b/config/golden-image-container-integration.yaml index 15b42deb..653ad3fd 100644 --- a/config/golden-image-container-integration.yaml +++ b/config/golden-image-container-integration.yaml @@ -83,11 +83,6 @@ toolchains: archive: pnpm-11.22.0.tgz download_url: https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz archive_sha256: 57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc - - name: rust - version: 1.97.1 - archive: rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz - download_url: https://static.rust-lang.org/dist/rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz - archive_sha256: 88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f - name: rustup version: 1.29.0 archive: rustup-init diff --git a/config/golden-image-container.yaml b/config/golden-image-container.yaml index 8730eb70..f3757efb 100644 --- a/config/golden-image-container.yaml +++ b/config/golden-image-container.yaml @@ -88,11 +88,6 @@ toolchains: archive: pnpm-11.22.0.tgz download_url: https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz archive_sha256: 57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc - - name: rust - version: 1.97.1 - archive: rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz - download_url: https://static.rust-lang.org/dist/rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz - archive_sha256: 88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f - name: rustup version: 1.29.0 archive: rustup-init diff --git a/config/golden-image-integration.yaml b/config/golden-image-integration.yaml index b4f603bf..bbd78eba 100644 --- a/config/golden-image-integration.yaml +++ b/config/golden-image-integration.yaml @@ -82,11 +82,6 @@ toolchains: archive: pnpm-11.22.0.tgz download_url: https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz archive_sha256: 57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc - - name: rust - version: 1.97.1 - archive: rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz - download_url: https://static.rust-lang.org/dist/rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz - archive_sha256: 88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f - name: rustup version: 1.29.0 archive: rustup-init diff --git a/config/golden-image.yaml b/config/golden-image.yaml index fe989fe9..7c7a7e78 100644 --- a/config/golden-image.yaml +++ b/config/golden-image.yaml @@ -82,11 +82,6 @@ toolchains: archive: pnpm-11.22.0.tgz download_url: https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz archive_sha256: 57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc - - name: rust - version: 1.97.1 - archive: rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz - download_url: https://static.rust-lang.org/dist/rust-1.97.1-x86_64-unknown-linux-gnu.tar.xz - archive_sha256: 88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f - name: rustup version: 1.29.0 archive: rustup-init diff --git a/internal/imagebuild/assets/provision.sh b/internal/imagebuild/assets/provision.sh index f2b1e8ee..b87fc3d1 100644 --- a/internal/imagebuild/assets/provision.sh +++ b/internal/imagebuild/assets/provision.sh @@ -219,9 +219,9 @@ toolchain_manifest="$(printf '%s' "${GHA_TOOLCHAINS_B64}" | base64 --decode)" jq -e 'type == "array"' <<<"${toolchain_manifest}" >/dev/null mapfile -t toolchain_names < <(jq -r '.[].name' <<<"${toolchain_manifest}") toolchain_set="$(printf '%s\n' "${toolchain_names[@]}" | LC_ALL=C sort | paste -sd, -)" -if [[ "${toolchain_set}" != bun,gh,go,rust,rustup,uv \ - && "${toolchain_set}" != bun,gh,go,node22,node24,node25,pnpm,rust,rustup,uv,yarn \ - && "${toolchain_set}" != bun,flutter,gh,go,node22,node24,node25,pnpm,rust,rustup,uv,yarn ]]; then +if [[ "${toolchain_set}" != bun,gh,go,rustup,uv \ + && "${toolchain_set}" != bun,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn \ + && "${toolchain_set}" != bun,flutter,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn ]]; then echo "toolchain manifest does not pin the exact baked set" >&2 exit 1 fi @@ -332,28 +332,6 @@ for toolchain_name in "${toolchain_names[@]}"; do ln -sfn "${package_root}/bin/pnpx.cjs" /usr/local/bin/pnpx [[ "$(pnpm --version)" == "${toolchain_version}" ]] ;; - rust) - # The standalone archive alone is not what the estate asks for. Every - # setup-system pins rust-toolchain.toml to a channel with rustfmt and - # clippy, and actions-rust-lang/setup-rust-toolchain resolves that through - # rustup -- so a system-wide rustc with no rustup, no clippy and no - # rustfmt is invisible to it and every job downloads a toolchain instead. - # Measured across the seven public anchors: 28 workflow requests for - # 1.98.0 and 7 for the 1.89 MSRV, against a baked 1.97.1. - # - # rustup owns the layout, so the archive installs into it rather than into - # /usr/local, and the extra channels are installed beside it. RUSTUP_HOME - # and CARGO_HOME are system paths so the toolchains are shared rather than - # re-fetched per job, and the shims land on PATH for jobs that never call - # the action at all. - tar --extract --xz --file "${toolchain_archive}" \ - --directory "${toolchain_scratch}" --no-same-owner --no-same-permissions - "${toolchain_scratch}/rust-${toolchain_version}-x86_64-unknown-linux-gnu/install.sh" \ - --prefix=/usr/local \ - --components=rustc,cargo,rust-std-x86_64-unknown-linux-gnu >/dev/null - [[ "$(rustc --version)" == "rustc ${toolchain_version} "* ]] - [[ "$(cargo --version)" == "cargo ${toolchain_version} "* ]] - ;; rustup) install -o root -g root -m 0755 "${toolchain_archive}" "${toolchain_scratch}/rustup-init" export RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo @@ -365,7 +343,10 @@ for toolchain_name in "${toolchain_names[@]}"; do # rust-toolchain.toml files name, so the action finds them already present. mapfile -t rust_channels < <(jq -r '.channels[]?' <<<"${entry}") rust_default="$(jq -r '.default_channel // ""' <<<"${entry}")" - [[ ${#rust_channels[@]} -gt 0 && -n "${rust_default}" ]] + if [[ ${#rust_channels[@]} -eq 0 || -z "${rust_default}" ]]; then + echo "rustup entry carries no channels or no default channel: the manifest names them, so the provisioning contract dropped them" >&2 + exit 1 + fi for channel in "${rust_channels[@]}"; do "${CARGO_HOME}/bin/rustup" toolchain install "${channel}" \ --profile minimal --component clippy --component rustfmt >/dev/null diff --git a/internal/imagebuild/assets/smoke-integration.sh b/internal/imagebuild/assets/smoke-integration.sh index 5612ee59..0b58b5d0 100644 --- a/internal/imagebuild/assets/smoke-integration.sh +++ b/internal/imagebuild/assets/smoke-integration.sh @@ -88,7 +88,7 @@ runner_tool_cache="$(jq -er .runner_tool_cache /etc/nddev/image-build.json)" [[ "${runner_tool_cache}" == /home/runner/actions-runner/_work/_tool ]] smoke_toolchains="$(printf '%s' "${GHA_TOOLCHAINS_B64}" | base64 --decode)" mapfile -t smoke_toolchain_names < <(jq -r '.[].name' <<<"${smoke_toolchains}") -[[ "$(printf '%s\n' "${smoke_toolchain_names[@]}" | LC_ALL=C sort | paste -sd, -)" == bun,gh,go,node22,node24,node25,pnpm,rust,uv,yarn ]] +[[ "$(printf '%s\n' "${smoke_toolchain_names[@]}" | LC_ALL=C sort | paste -sd, -)" == bun,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn ]] for smoke_toolchain in "${smoke_toolchain_names[@]}"; do entry="$(jq -ce --arg name "${smoke_toolchain}" '.[] | select(.name == $name)' <<<"${smoke_toolchains}")" expected_version="$(jq -er .version <<<"${entry}")" @@ -128,9 +128,26 @@ for smoke_toolchain in "${smoke_toolchain_names[@]}"; do fi ;; pnpm) [[ "$(pnpm --version)" == "${expected_version}" ]] ;; - rust) - [[ "$(rustc --version)" == "rustc ${expected_version} "* ]] - [[ "$(cargo --version)" == "cargo ${expected_version} "* ]] + rustup) + # rustup owns the whole Rust surface: the estate pins channels in + # rust-toolchain.toml, actions-rust-lang/setup-rust-toolchain resolves + # them through rustup, and the shims on PATH serve jobs that never call + # the action. Every pinned channel must already hold clippy and rustfmt, + # or the per-job download this image exists to remove comes back. + [[ "$(rustup --version 2>/dev/null)" == "rustup ${expected_version} "* ]] + [[ "$(stat --format='%U' -- /usr/local/rustup)" == root ]] + [[ "$(stat --format='%U' -- /usr/local/cargo)" == root ]] + grep -q '^RUSTUP_HOME=/usr/local/rustup$' /etc/environment + mapfile -t smoke_rust_channels < <(jq -r '.channels[]?' <<<"${entry}") + smoke_rust_default="$(jq -er '.default_channel' <<<"${entry}")" + [[ ${#smoke_rust_channels[@]} -gt 0 ]] + for smoke_rust_channel in "${smoke_rust_channels[@]}"; do + [[ "$(rustup run "${smoke_rust_channel}" rustc --version)" == "rustc ${smoke_rust_channel}"* ]] + [[ "$(rustup run "${smoke_rust_channel}" cargo clippy --version)" == clippy* ]] + [[ "$(rustup run "${smoke_rust_channel}" rustfmt --version)" == rustfmt* ]] + done + [[ "$(rustc --version)" == "rustc ${smoke_rust_default}"* ]] + [[ "$(cargo --version)" == "cargo "* ]] ;; uv) [[ "$(uv --version)" == "uv ${expected_version}"* ]] diff --git a/internal/imagebuild/assets/smoke.sh b/internal/imagebuild/assets/smoke.sh index 166c4bb7..dc76ab17 100644 --- a/internal/imagebuild/assets/smoke.sh +++ b/internal/imagebuild/assets/smoke.sh @@ -64,9 +64,9 @@ runner_tool_cache="$(jq -er .runner_tool_cache /etc/nddev/image-build.json)" smoke_toolchains="$(printf '%s' "${GHA_TOOLCHAINS_B64}" | base64 --decode)" mapfile -t smoke_toolchain_names < <(jq -r '.[].name' <<<"${smoke_toolchains}") smoke_toolchain_set="$(printf '%s\n' "${smoke_toolchain_names[@]}" | LC_ALL=C sort | paste -sd, -)" -[[ "${smoke_toolchain_set}" == bun,gh,go,rust,uv \ - || "${smoke_toolchain_set}" == bun,gh,go,node22,node24,node25,pnpm,rust,uv,yarn \ - || "${smoke_toolchain_set}" == bun,flutter,gh,go,node22,node24,node25,pnpm,rust,uv,yarn ]] +[[ "${smoke_toolchain_set}" == bun,gh,go,rustup,uv \ + || "${smoke_toolchain_set}" == bun,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn \ + || "${smoke_toolchain_set}" == bun,flutter,gh,go,node22,node24,node25,pnpm,rustup,uv,yarn ]] for smoke_toolchain in "${smoke_toolchain_names[@]}"; do entry="$(jq -ce --arg name "${smoke_toolchain}" '.[] | select(.name == $name)' <<<"${smoke_toolchains}")" expected_version="$(jq -er .version <<<"${entry}")" @@ -115,9 +115,26 @@ for smoke_toolchain in "${smoke_toolchain_names[@]}"; do fi ;; pnpm) [[ "$(pnpm --version)" == "${expected_version}" ]] ;; - rust) - [[ "$(rustc --version)" == "rustc ${expected_version} "* ]] - [[ "$(cargo --version)" == "cargo ${expected_version} "* ]] + rustup) + # rustup owns the whole Rust surface: the estate pins channels in + # rust-toolchain.toml, actions-rust-lang/setup-rust-toolchain resolves + # them through rustup, and the shims on PATH serve jobs that never call + # the action. Every pinned channel must already hold clippy and rustfmt, + # or the per-job download this image exists to remove comes back. + [[ "$(rustup --version 2>/dev/null)" == "rustup ${expected_version} "* ]] + [[ "$(stat --format='%U' -- /usr/local/rustup)" == root ]] + [[ "$(stat --format='%U' -- /usr/local/cargo)" == root ]] + grep -q '^RUSTUP_HOME=/usr/local/rustup$' /etc/environment + mapfile -t smoke_rust_channels < <(jq -r '.channels[]?' <<<"${entry}") + smoke_rust_default="$(jq -er '.default_channel' <<<"${entry}")" + [[ ${#smoke_rust_channels[@]} -gt 0 ]] + for smoke_rust_channel in "${smoke_rust_channels[@]}"; do + [[ "$(rustup run "${smoke_rust_channel}" rustc --version)" == "rustc ${smoke_rust_channel}"* ]] + [[ "$(rustup run "${smoke_rust_channel}" cargo clippy --version)" == clippy* ]] + [[ "$(rustup run "${smoke_rust_channel}" rustfmt --version)" == rustfmt* ]] + done + [[ "$(rustc --version)" == "rustc ${smoke_rust_default}"* ]] + [[ "$(cargo --version)" == "cargo "* ]] ;; uv) [[ "$(uv --version)" == "uv ${expected_version}"* ]] diff --git a/internal/imagebuild/orchestrator.go b/internal/imagebuild/orchestrator.go index 4c63cb53..bd49ab71 100644 --- a/internal/imagebuild/orchestrator.go +++ b/internal/imagebuild/orchestrator.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "github.com/NDDev-OpenNetwork/github-actions/internal/imagemanifest" "github.com/NDDev-OpenNetwork/github-actions/internal/imageplan" ) @@ -296,6 +297,26 @@ type guestToolchain struct { Version string `json:"version"` Archive string `json:"archive"` ArchiveSHA256 string `json:"archive_sha256"` + // Channels and DefaultChannel ride along for version managers. Dropping + // them here once produced a rustup with no toolchains: the manifest named + // the channels, every manifest gate passed, and the builder still died + // because this projection was the one place they were not carried. + Channels []string `json:"channels,omitempty"` + DefaultChannel string `json:"default_channel,omitempty"` +} + +// guestToolchainFromPlan is the single projection from a manifest toolchain to +// the guest contract. Provision receives the pushed archive path; smoke +// receives none, because it verifies installed state rather than archives. +func guestToolchainFromPlan(toolchain imagemanifest.Toolchain, archivePath string) guestToolchain { + return guestToolchain{ + Name: toolchain.Name, + Version: toolchain.Version, + Archive: archivePath, + ArchiveSHA256: toolchain.ArchiveSHA256, + Channels: toolchain.Channels, + DefaultChannel: toolchain.DefaultChannel, + } } func encodeToolchains(toolchains []guestToolchain) (string, error) { @@ -573,12 +594,8 @@ exit 1`); err != nil { if _, err = o.Runner.Run(ctx, o.incus(plan.Project, "file", "push", artifacts.Toolchains[toolchain.Name], destination, "--mode", "0600")...); err != nil { return "", "", fmt.Errorf("push verified %s toolchain archive: %w", toolchain.Name, err) } - toolchainRequests = append(toolchainRequests, guestToolchain{ - Name: toolchain.Name, - Version: toolchain.Version, - Archive: "/var/tmp/" + toolchain.Archive, - ArchiveSHA256: toolchain.ArchiveSHA256, - }) + toolchainRequests = append(toolchainRequests, + guestToolchainFromPlan(toolchain, "/var/tmp/"+toolchain.Archive)) } encodedToolchains, err := encodeToolchains(toolchainRequests) if err != nil { @@ -800,11 +817,7 @@ func (o Orchestrator) smoke(ctx context.Context, plan imageplan.Plan, fingerprin smoke, _ := scripts.ReadFile(smokeName) smokeToolchains := make([]guestToolchain, 0, len(plan.Toolchains)) for _, toolchain := range plan.Toolchains { - smokeToolchains = append(smokeToolchains, guestToolchain{ - Name: toolchain.Name, - Version: toolchain.Version, - ArchiveSHA256: toolchain.ArchiveSHA256, - }) + smokeToolchains = append(smokeToolchains, guestToolchainFromPlan(toolchain, "")) } encodedToolchains, err := encodeToolchains(smokeToolchains) if err != nil { diff --git a/internal/imagebuild/orchestrator_test.go b/internal/imagebuild/orchestrator_test.go index 397aebed..70ae123c 100644 --- a/internal/imagebuild/orchestrator_test.go +++ b/internal/imagebuild/orchestrator_test.go @@ -164,7 +164,7 @@ func TestRecipeFingerprintIsDeterministic(t *testing.T) { // alias is part of the recipe, so a manifest whose contents changed under an // unchanged alias would otherwise ask the builder to produce different bytes // for a name that is already promoted. - if first != "sha256:3d89d31c7c5a423238a6888b2b4c4d894f22fcb91e0d6ea76fd689d0c5a09b9b" { + if first != "sha256:6b92afb396a9d31a6772e489343006f2ce05c93a59a641701e2bf49ff8fe23fc" { t.Fatalf("deployed standard recipe fingerprint drifted: %q", first) } smoke, err := SmokeFingerprint(plan) @@ -689,3 +689,42 @@ func TestFailedBuildPreservesTheBuilderOnlyWhenAsked(t *testing.T) { } } } + +// TestGuestToolchainContractCarriesChannels guards the projection between the +// manifest and the guest scripts. The channels were once present in the +// manifest, validated by imagemanifest tests, and still absent inside the +// builder, because this projection dropped them; provision then failed with no +// toolchain installed. Both the provisioning and the smoke contract must carry +// them. +func TestGuestToolchainContractCarriesChannels(t *testing.T) { + t.Parallel() + + pinned := imagemanifest.Toolchain{ + Name: "rustup", + Version: "1.29.0", + Archive: "rustup-init", + ArchiveSHA256: strings.Repeat("a", 64), + Channels: []string{"1.98.0", "1.89"}, + DefaultChannel: "1.98.0", + } + + for name, projected := range map[string]guestToolchain{ + "provision": guestToolchainFromPlan(pinned, "/var/tmp/"+pinned.Archive), + "smoke": guestToolchainFromPlan(pinned, ""), + } { + encoded, err := encodeToolchains([]guestToolchain{projected}) + if err != nil { + t.Fatalf("%s: encode: %v", name, err) + } + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + t.Fatalf("%s: decode: %v", name, err) + } + contract := string(decoded) + for _, needle := range []string{`"channels":["1.98.0","1.89"]`, `"default_channel":"1.98.0"`} { + if !strings.Contains(contract, needle) { + t.Fatalf("%s contract %s lost %s", name, contract, needle) + } + } + } +} diff --git a/internal/imagemanifest/manifest_test.go b/internal/imagemanifest/manifest_test.go index 52bea667..c05f1c03 100644 --- a/internal/imagemanifest/manifest_test.go +++ b/internal/imagemanifest/manifest_test.go @@ -42,7 +42,7 @@ func TestRepositoryManifestIsValidAndPinned(t *testing.T) { if !strings.HasPrefix(fingerprint, "sha256:") || len(fingerprint) != len("sha256:")+64 { t.Fatalf("unexpected fingerprint %q", fingerprint) } - if fingerprint != "sha256:02591ff082938633b0435312ef80f7a397021f80fe155fa62ece7cc6026e8e70" { + if fingerprint != "sha256:d7fdc01a61f2afc9991ef658511ea3cf73f73c251d0043c04c392f06b6397d1a" { t.Fatalf("standard manifest fingerprint drifted: %q", fingerprint) } } @@ -77,7 +77,6 @@ func assertBakedToolchains(t *testing.T, manifest Manifest) { "node24": {"24.19.0", "14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647"}, "node25": {"25.9.0", "1d8db7d6e291d167e8c467ae4094be175e1a0b3969c7ae1f8955b9f7824f7b2e"}, "pnpm": {"11.22.0", "57a97e6f23a3faffc03153a4ef8c770a0552612b8640aebe39bfdd5754d0ebdc"}, - "rust": {"1.97.1", "88f28fa9af20594179f85d6df67078dfd6fa93e2f6da5e1e9b0ac4997988ca4f"}, "rustup": {"1.29.0", "4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10"}, "uv": {"0.11.30", "04bc7d180d6138bf6dc08387acf507a823f397a98fea55da36b0ccc7fbce3b68"}, "yarn": {"4.18.0", "606e7e2dfc8bcc24e1b3a70a1043288a271ad2cc71cf42248fadc25f5938a497"}, diff --git a/internal/imagemanifest/types.go b/internal/imagemanifest/types.go index 45dea67c..c1ae04af 100644 --- a/internal/imagemanifest/types.go +++ b/internal/imagemanifest/types.go @@ -116,12 +116,13 @@ type BrowserSmoke struct { // already on PATH, and actions/setup-go resolves a pre-seeded runner tool cache, // so a complete set turns per-job toolchain installation into a no-op. func BakedToolchains() []string { - // rustup is baked beside rust, not instead of it. A system-wide rustc - // satisfies a job that calls cargo directly; it is invisible to - // actions-rust-lang/setup-rust-toolchain, which resolves the channel in - // rust-toolchain.toml through rustup. Both are needed because the estate - // does both. - return []string{"bun", "gh", "go", "node22", "node24", "node25", "pnpm", "rust", "rustup", "uv", "yarn"} + // rustup owns the whole Rust surface. A standalone rust tarball was baked + // beside it once, and the rustup shims overwrote its /usr/local/bin + // binaries during provisioning, so the "system" rust the recipe promised + // was unreachable in the shipped image. The default channel serves jobs + // that call cargo directly, and actions-rust-lang/setup-rust-toolchain + // resolves rust-toolchain.toml channels through rustup either way. + return []string{"bun", "gh", "go", "node22", "node24", "node25", "pnpm", "rustup", "uv", "yarn"} } // OptionalToolchains may be pinned by an image that needs them and omitted by diff --git a/internal/imageplan/plan_test.go b/internal/imageplan/plan_test.go index 0976bb57..7bfa0007 100644 --- a/internal/imageplan/plan_test.go +++ b/internal/imageplan/plan_test.go @@ -82,7 +82,7 @@ func TestBuildProducesDeterministicBoundedPlan(t *testing.T) { } // The baked set plus the optional Flutter SDK this manifest pins, still // sorted: the plan's ordering is what makes the recipe fingerprint stable. - if !slices.Equal(names, []string{"bun", "flutter", "gh", "go", "node22", "node24", "node25", "pnpm", "rust", "rustup", "uv", "yarn"}) { + if !slices.Equal(names, []string{"bun", "flutter", "gh", "go", "node22", "node24", "node25", "pnpm", "rustup", "uv", "yarn"}) { t.Fatalf("plan toolchains are not the canonical sorted set: %v", names) } if first.CompilerCache.Name != "sccache" || first.CompilerCache.Version != "v0.17.0" || diff --git a/internal/repositorycontract/external_downloads_test.go b/internal/repositorycontract/external_downloads_test.go index da72aee2..852e3b46 100644 --- a/internal/repositorycontract/external_downloads_test.go +++ b/internal/repositorycontract/external_downloads_test.go @@ -68,7 +68,7 @@ func TestExternalDownloadFallbackMarkersRemainBounded(t *testing.T) { "--retry 2", "verify_candidate", "nddev_tool_cache_event=", }, "scripts/install-benchmark-toolchain.sh": { - "--retry 2", "toolchain SHA-256 mismatch", "rustc 1.97.1", + "--retry 2", "toolchain SHA-256 mismatch", "rustc 1.98.0", }, "internal/garmproviderincus/provider/incus.go": { "--retry 2", "validateRunnerTool", diff --git a/scripts/install-benchmark-toolchain.sh b/scripts/install-benchmark-toolchain.sh index 43e15359..b3e6aac5 100755 --- a/scripts/install-benchmark-toolchain.sh +++ b/scripts/install-benchmark-toolchain.sh @@ -68,8 +68,8 @@ install -d -m 0700 "${install_root}" "${binary_root}" case "${toolchain}" in rust) - if [[ "$(rustc --version 2>/dev/null || true)" == "rustc 1.97.1 "* && - "$(cargo --version 2>/dev/null || true)" == "cargo 1.97.1 "* ]]; then + if [[ "$(rustc --version 2>/dev/null || true)" == "rustc 1.98.0 "* && + "$(cargo --version 2>/dev/null || true)" == "cargo 1.98.0 "* ]]; then exit 0 fi : "${CARGO_HOME:?CARGO_HOME is required for Rust installation}" @@ -85,12 +85,12 @@ case "${toolchain}" in "${rustup_init}" chmod 0700 "${rustup_init}" export RUSTUP_HOME="${rustup_home}" - "${rustup_init}" --default-toolchain 1.97.1 --profile minimal --no-modify-path -y + "${rustup_init}" --default-toolchain 1.98.0 --profile minimal --no-modify-path -y printf 'RUSTUP_HOME=%s\n' "${rustup_home}" >>"${GITHUB_ENV}" printf '%s/bin\n' "${CARGO_HOME}" >>"${GITHUB_PATH}" export PATH="${CARGO_HOME}/bin:${PATH}" - [[ "$(rustc --version)" == "rustc 1.97.1 "* ]] - [[ "$(cargo --version)" == "cargo 1.97.1 "* ]] + [[ "$(rustc --version)" == "rustc 1.98.0 "* ]] + [[ "$(cargo --version)" == "cargo 1.98.0 "* ]] ;; uv) if [[ "$(uv --version 2>/dev/null || true)" == "uv 0.11.30"* ]]; then