Skip to content

soldr rustup: drop-in passthrough + project-level soldr toolchain prepare #331

Description

@zackees

0. Design principle: drop-in syntax

The strongest argument for adding rustup to soldr's known-tool registry isn't ergonomics — it's preserving muscle memory. soldr already follows a strict drop-in convention: soldr cargo build is identical to cargo build except scoped to the pin and routed through zccache. The same contract should hold for rustup: soldr rustup target add x86_64-unknown-linux-musl should be identical to rustup target add x86_64-unknown-linux-musl except scoped to the project's pinned toolchain.

This rules out an entire class of "convenience" surface. No soldr target add, no soldr component add, no soldr-only verb that re-invents something rustup already spells. Every new verb soldr introduces has to earn its keep against this test. The only soldr-native verbs that earn it are the ones with no rustup equivalent: soldr toolchain prepare (reads soldr's project manifest; has no rustup analog) and soldr doctor (diagnostic). Everything else stays as soldr <wrapped-tool> <args> and behaves exactly like the underlying tool.

1. The proposal, in one paragraph

Make rustup a first-class soldr subcommand alongside cargo, rustc, clippy-driver, and rustfmt. soldr rustup <args> shells out to the system rustup binary with the same arguments, but with soldr in charge of which toolchain the call targets — so soldr rustup target add x86_64-unknown-linux-musl always installs against the pinned toolchain that soldr is enforcing, not against whatever rustup considers "active." On top of that passthrough, add soldr toolchain prepare: read the project's declared toolchain footprint and preload everything in one shot, before any build runs.

2. Ramifications

2.1 Toolchain-pin coherence (correctness, not just ergonomics)

A developer machine usually has more than one Rust toolchain installed: a stable from rustup-init, a project-pinned 1.94.1, maybe a nightly for fuzzing, maybe an older pin held open by another project. When a user runs bare rustup target add x86_64-unknown-linux-musl, rustup picks the active toolchain — whatever the user last set as default, or whatever rust-toolchain.toml resolves in the current directory. This is fragile. If the user is one directory up, or a parent has a rust-toolchain.toml that shadows the project's pin, the target lands on the wrong toolchain. The build then fails with a confusing "target not installed" error, and rustup target list --installed shows the target is installed — just somewhere else. soldr rustup target add ... removes this whole failure mode by forcing the call to be rustup target add --toolchain <pinned> <triple>. The pin is the source of truth; soldr enforces it.

2.2 Eager preparation eliminates mid-build ambush failures

Today, the discovery loop for a missing cross target is:

  1. Run cargo build --target x86_64-unknown-linux-musl.
  2. Cargo invokes rustc.
  3. Rustc errors: can't find crate for 'std'.
  4. Developer runs rustup target add ..., retries.
  5. Now a different target is missing because a build-script targets the host and the binary targets musl, or a component like rust-src is required by a plugin that wasn't installed.

Every one of these failures costs minutes on a slow machine and a re-queued job in CI. They're all preventable: the project already knows which targets, components, and plugins it needs. A soldr toolchain prepare step run before any compilation moves these costs from "discovered painfully mid-build" to "paid once during a known setup phase." Same shift cargo fetch made for dependencies: not magic, just doing the inevitable work in the right place.

2.3 Single source of truth for a project's toolchain footprint

A non-trivial Rust project's toolchain footprint is more than "rustc." It's: a specific toolchain pin, clippy + rustfmt for CI, rust-src for cargo-fuzz / cargo-careful, one or more cross targets, possibly a nightly for miri, and a handful of cargo plugins (cargo-zigbuild, cargo-deny, cargo-nextest). Today, that footprint is scattered across rust-toolchain.toml, .cargo/config.toml, CI workflow YAML files, README setup blocks, and tribal knowledge. New contributors hit a different missing piece every time they try a different command. A soldr-recognized manifest — rust-toolchain.toml (which already supports targets and components natively in its [toolchain] table) plus an optional [soldr.plugins] section for cargo plugins — collapses that into one auditable spec, and soldr toolchain prepare enforces it.

2.4 Better error surface

soldr: tool not found: rustup: no repository on crates.io is technically accurate and entirely useless. A user typing soldr rustup target add ... expects either success or a clear "rustup doesn't ship from crates.io, install it from rustup.rs" — not a message that suggests soldr looked for rustup as if it were serde. Making rustup a known special case lets soldr produce errors at the right semantic level: "rustup binary not found on PATH," "pinned toolchain 1.94.1 isn't installed; run soldr toolchain install," "rustup >= 1.26 required, found 1.20."

2.5 Reproducibility across machines

"Does this build work on your machine?" today depends on off-band state: which rustup toolchains are installed, which targets and components those toolchains have, which cargo plugins are on PATH, which zig is installed. A soldr-mediated bootstrap that owns rustup means the answer to "what state does this project need from the toolchain layer?" is a single command's output, identical on every machine that runs it. setup-soldr already provides this property for CI; bringing it home to local dev closes a real reproducibility gap.

3. Examples

3.1 The dev-docker loop that motivated this

The ./dev script in the project that motivated this issue today reads:

if command -v rustup >/dev/null 2>&1; then
    rustup target add "${TARGET_TRIPLE}"
else
    echo "WARN: rustup not on PATH..." >&2
fi

soldr cargo install cargo-zigbuild --locked
( cd core && soldr cargo zigbuild --release --target "${TARGET_TRIPLE}" --locked )

Three setup checks and one bare-rustup invocation because soldr doesn't own toolchain prep yet. With drop-in soldr rustup plus a small project manifest, the intermediate form is every command consistently prefixed:

soldr rustup target add "${TARGET_TRIPLE}"
soldr cargo install cargo-zigbuild --locked
( cd core && soldr cargo zigbuild --release --target "${TARGET_TRIPLE}" --locked )

And once a manifest exists, the bootstrap collapses to:

soldr toolchain prepare
( cd core && soldr cargo zigbuild --release --target x86_64-unknown-linux-musl --locked )

3.2 CI via setup-soldr

Multi-target CI today, in full:

- uses: zackees/setup-soldr@v0
  with: { cache: true }
- run: rustup target add x86_64-unknown-linux-musl
- run: rustup target add x86_64-pc-windows-gnu
- run: rustup component add clippy rustfmt
- run: cargo install cargo-zigbuild --locked
- run: cargo install cargo-nextest --locked
- run: pip install ziglang
- run: cargo zigbuild --release --target x86_64-unknown-linux-musl
- run: cargo zigbuild --release --target x86_64-pc-windows-gnu
- run: cargo nextest run
- run: cargo clippy -- -D warnings

The drop-in form preserves the shape but routes every tool through soldr:

- uses: zackees/setup-soldr@v0
  with: { cache: true }
- run: soldr rustup target add x86_64-unknown-linux-musl
- run: soldr rustup target add x86_64-pc-windows-gnu
- run: soldr rustup component add clippy rustfmt
- run: soldr cargo install cargo-zigbuild --locked
- run: soldr cargo install cargo-nextest --locked
- run: pip install ziglang
- run: soldr cargo zigbuild --release --target x86_64-unknown-linux-musl
- run: soldr cargo zigbuild --release --target x86_64-pc-windows-gnu
- run: soldr cargo nextest run
- run: soldr cargo clippy -- -D warnings

With manifest + prepare, the provisioning lines collapse:

- uses: zackees/setup-soldr@v0
  with: { cache: true, prepare-toolchain: true }
- run: soldr cargo zigbuild --release --target x86_64-unknown-linux-musl
- run: soldr cargo zigbuild --release --target x86_64-pc-windows-gnu
- run: soldr cargo nextest run
- run: soldr cargo clippy -- -D warnings

3.3 New-contributor onboarding

Today: clone, cargo build works, try ./dev, fails because pin is wrong, install rustup, re-run, target missing, install target, re-run, plugin missing, install, re-run, ziglang missing, install, finally works. Five distinct discovery failures.

With soldr toolchain prepare: clone, one bootstrap command (installs rustup-via-soldr if missing, pins toolchain, adds declared targets and components, installs declared cargo plugins, fetches zig), ./dev works.

3.4 Multi-target release workflow

A release producing musl-Linux, Windows-GNU, and macOS binaries today has to remember three rustup target add calls, the cargo-zigbuild install, and the right env vars per target. With a manifest declaring the three triples, the release is soldr toolchain prepare followed by three soldr cargo zigbuild --target <triple> calls. "Which targets are part of this release?" lives in one place rather than three CI scripts that have to be kept in sync.

4. How this fits the existing feature set

4.1 The "well-known managed tools" registry pattern

soldr already treats cargo / rustc / clippy-driver / rustfmt as well-known names that aren't fetched from crates.io. Adding rustup to that registry is a small, principled extension of an existing pattern. The same registry could absorb other commonly-needed-but-not-on-crates.io tools (zig, sccache, mold, the wasm-pack installer) in future expansions, each carrying the same "soldr knows how to find or fetch this; you don't have to know the install command" property — and each obeying the drop-in rule.

4.2 Composes with soldr cargo install

soldr already routes cargo install through itself, which means cargo plugins installed via soldr can land in a soldr-managed location instead of ~/.cargo/bin and be pinned per project. soldr toolchain prepare reads the manifest's plugin list and calls the existing soldr cargo install primitive in a loop. No new install path, no second source of truth — one orchestrator over an existing primitive.

4.3 zccache parallel: cache the toolchain layer the way we cache compilation

zccache already caches compile output across cargo build, cargo test, and cargo clippy so switching between them isn't wasteful. Toolchain artifacts — downloaded targets, downloaded components, downloaded cargo-plugin binaries — are the same shape of problem: expensive to fetch, rarely changing, shared across invocations. soldr is already in the cache-management business; extending zccache (or a sibling cache) to hold these artifacts is a natural step.

4.4 setup-soldr exposes a declarative subset

The companion issue at zackees/setup-soldr#104 already proposes auto-bootstrap on CI. With soldr toolchain prepare as the underlying primitive, setup-soldr becomes the declarative front end (YAML inputs → spec) and soldr becomes the engine (spec → installed state). The action stays the relaxed/CI-friendly side of the pair; the CLI stays the strict/explicit side. Same division of labor; sharper boundary between front end and engine.

5. Subcommand surface (minimal)

soldr rustup <args>            # drop-in passthrough, scoped to the project pin
soldr toolchain install        # ensure the pinned toolchain itself is installed
soldr toolchain prepare        # read manifest, install everything declared in one shot
soldr doctor                   # report drift between manifest and installed state

That's it. No soldr target add, no soldr component add — those are soldr rustup target add and soldr rustup component add and behave identically to bare rustup except for the pin scoping. The manifest is the existing rust-toolchain.toml (already supports targets and components in [toolchain]) plus an optional [soldr.plugins] section. No new file needed for the common case.

6. Target storage: global, not project-scoped

When soldr rustup target add x86_64-unknown-linux-musl installs a cross-compile target, where on disk should the rustlib live? Three plausible answers:

  • (A) Project-level: install into a project-private rustup home.
  • (B) Global: rustup's native layout — ~/.rustup/toolchains/<channel>-<host>/lib/rustlib/<target>/.
  • (C) soldr-owned cache + symlinks: dedup centrally, link into per-project rustup homes.

Answer: (B). Global, rustup-native, no symlinks.

Why not project-level (A)

A rustlib is keyed by (toolchain_version, target_triple) — that tuple is already globally unique, with no project-specific variant possible. Project A using 1.94.1 + musl and project B using 1.94.1 + musl would store the exact same bytes twice. Five projects × three targets × two toolchains is roughly 1.5 GB of duplicated std rustlibs for a class of artifact (50–150 MB each) where dedup is free if you don't fight rustup. Cold install per project, no sharing.

Why not symlinks-into-project (C)

The symlink approach buys nothing meaningful and breaks real invariants:

  • Rustup writes into the toolchain directory during component/target operations and expects to own it. Symlinking a "shared" rustlib into multiple project-private homes risks one project's rustup component add mutating another's expected state.
  • Cross-volume symlinks on Windows are restricted (require elevation or developer mode).
  • The only thing the symlink would buy is ls visibility — but rustup target list --installed --toolchain <pin> already answers "what's installed for this pin" and the manifest answers "what should be." Two clear sources of truth beats a symlink farm.

Why global (B) is right

Rustup natively dedupes by toolchain. A target rustlib at ~/.rustup/toolchains/1.94.1-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/ is shared by every project on that machine using 1.94.1 + musl. That's the property you want — fighting it costs complexity for no win on a class of artifact too small to matter (50–150 MB vs. multi-GB registry-src / workspace target trees).

What soldr earns at this layer

Not storage. Authority and reaping.

  • Authority: soldr toolchain prepare reads each project's manifest (rust-toolchain.toml + [soldr.plugins]) and shells out rustup target add --toolchain <pin> <triple> for missing entries. Global install, project-scoped enforcement.
  • Reaping: soldr's target registry (~/.soldr/state.redb, already tracking workspace target/ dirs for GC) gets extended to record which (toolchain, target) pairs each project's manifest references. That gives GC a signal it doesn't have today.
  • New GC kind under soldr#323's taxonomy: rustup_target_rustlib, safety class derived (re-fetchable from dist server). Eligibility: rustlib for a (toolchain, target) pair where no registered project manifest references that pair AND last-used > some threshold. Belongs in soldr gc sweep's aggressive tier — the reclaim volume is small relative to other kinds, so the value is correctness (cleaning up after deprecated cross-compile targets) rather than reclaim bytes.

Disk-cost framing

Class Typical per-machine size GC priority
Workspace target/ (per workspace) 200 MB – 4 GB high
~/.cargo/registry/src/ 1 – 8 GB high
~/.cargo/git/checkouts/ 0 – 2 GB medium
Target rustlibs (rustup_target_rustlib) 50 MB – 1 GB total low — but adds correctness signal

Adding rustup_target_rustlib to GC is mostly about hygiene (manifest-driven cleanup), not pressure relief.

7. Non-goals

  • Replacing rustup. soldr shells out to the system rustup binary; it does not reimplement toolchain installation.
  • Implicit toolchain installs at build time. soldr cargo build --target <triple> should still fail loudly if the target isn't installed. Bootstrap is a separate, explicit step — that's the whole point of the eager-prep model. Implicit installs would re-introduce the ambush failure mode in a new disguise.
  • Convenience verbs that duplicate rustup spellings. Excluded by the design principle in §0.
  • Project-local rustup homes or symlink-based target sharing. See §6 — global storage in rustup's native layout, with soldr acting as authority + reaper.
  • Managing non-Rust language toolchains as a general framework. The rustup / zig / sccache extensions are pragmatic exceptions for tools that are functionally part of a Rust cross-compile pipeline. soldr isn't trying to become asdf.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions