Cross-compiling with soldr

August 14, 2026 · View on GitHub

This guide documents the currently-working recipes for cross-compiling between Linux and Windows targets through soldr. It addresses the exploration tracked in issue #329.

soldr owns the complete target lifecycle. The consumer selects a target; soldr installs Rust std, prepares the compiler/linker and SDK/sysroot, and merges required target flags with project RUSTFLAGS, CFLAGS, and CXXFLAGS. The same preparation is used by build, clippy, test compilation, nextest archives, and PEP 517 wheels.

If you want a more permissive, CI-side experience that can install system packages, see zackees/setup-soldr's cross-targets: input instead.

Windows hosts: the bash-flavored docker-prebuilt + cross-compile recipes on this page assume Git Bash / MSYS2 / Cygwin / WSL on PATH. soldr does NOT bootstrap the POSIX shell layer — see docs/WINDOWS_PREREQS.md for the install matrix + the common cygpath: command not found / docker bind-mount error → fix mapping (soldr#885).


TL;DR

You wantUse
Any canonical targetsoldr build --target <alias-or-triple>
Windows x64 → Windows GNUmanaged MinGW-w64 GCC + GNU syslibs (Section 1)
Linux → Windows MSVCsoldr build (Section 2)
Windows/Linux -> Linuxsoldr build --target <linux-triple>
Windows/Linux -> Macsoldr build + target-shaped Apple SDK (Section 1a)
Declare cross targets up-front[toolchain].targets + [soldr.plugins] (Section 3)

Canonical target aliases

ci/canonical-targets.json is the machine-readable authority for the target, alias, CI, release, and catalogue contract. Raw Rust triples remain accepted.

AliasRust targetCI validationRelease
win-x64x86_64-pc-windows-msvcCross-build + native runShipped
win-arm64aarch64-pc-windows-msvcCross-build + native runShipped
mac-x64x86_64-apple-darwinCross-build + native runShipped
mac-arm64aarch64-apple-darwinCross-build + native runShipped
linux-x64x86_64-unknown-linux-gnuNative build + runShipped
linux-arm64aarch64-unknown-linux-gnuCross-build + native runShipped
linux-x64-muslx86_64-unknown-linux-muslCross-build only (host-identical runner)Shipped
linux-arm64-muslaarch64-unknown-linux-muslCross-build + native runShipped

The target is the only toolchain choice in normal builds:

soldr prepare --target linux-arm64
soldr build --target linux-arm64 --release
soldr cargo clippy --target linux-arm64 --all-targets
soldr cargo test --no-run --target linux-arm64
soldr env --target linux-arm64 --json

Legacy wrapper commands are retained only for diagnostic comparisons and are never the default CI or release recipe.


1. Windows x64 → Windows GNU via managed MinGW-w64 GCC

On Windows x64 hosts, soldr prepare --target x86_64-pc-windows-gnu downloads a pinned WinLibs MinGW-w64 GCC bundle from the soldr-toolchain catalogue, prepends its bin/ directory to PATH, and exports the target-scoped Cargo/cc-rs variables needed by build scripts: CC_x86_64_pc_windows_gnu, CXX_x86_64_pc_windows_gnu, AR_x86_64_pc_windows_gnu, RANLIB_x86_64_pc_windows_gnu, WINDRES_x86_64_pc_windows_gnu, and CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER.

The same prepare/build path materializes GNU-shaped managed syslib rows (windows-x64-gnu) for the C dependencies soldr injects: zstd, sqlite, mimalloc, zlib-ng, lzma, and bzip2. A Windows GNU build must not consume windows-x64 MSVC syslibs.

Recipe

rustup target add x86_64-pc-windows-gnu

soldr prepare --target x86_64-pc-windows-gnu
where gcc
gcc --version

soldr build --target x86_64-pc-windows-gnu --release

In GitHub Actions, pass --github-env $env:GITHUB_ENV so later steps inherit the same compiler/linker/pkg-config environment:

soldr prepare --target x86_64-pc-windows-gnu --github-env $env:GITHUB_ENV

Scope is intentionally narrow: first-class managed MinGW provisioning currently supports only x86_64-pc-windows-gnu on Windows x64 hosts. i686-pc-windows-gnu, aarch64-pc-windows-gnullvm, and other Windows GNU-family targets are follow-ups. Linux hosts are not a blessed Windows GNU path in soldr; do not use cargo-zigbuild as a substitute for this target.


1a. Canonical Linux and macOS targets through soldr build

Windows contributors can produce Linux and macOS binaries locally instead of pushing a branch and waiting on CI, but the two target families now use one blessed build surface:

  • GNU Linux targets use catalogue-backed GCC/binutils/glibc-2.17 sysroot bundles internally through soldr build. The published baseline is glibc 2.17; --target <triple>.2.17 selects it explicitly, while an unsupported floor is rejected rather than silently weakened. Soldr selects the correct x86_64 or aarch64 bundle from the requested triple, verifies the catalogue SHA-256, and exports the target compiler/linker, CMake, and pkg-config sysroot environment. Neither zig, cargo-zigbuild, nor ziglang is on this blessed GNU path.
  • musl Linux targets use separate catalogue-backed GCC/binutils/musl static CRT bundles for x86_64-unknown-linux-musl and aarch64-unknown-linux-musl. The compiler, linker, CMake, and pkg-config environment all resolve under the verified managed root; normal musl builds never download or execute Zig, cargo-zigbuild, or ziglang. SOLDR_USE_LEGACY_ZIGBUILD is removed (soldr#2519); there is no Zig route to fall back to.
  • macOS targets use soldr build --target <apple-triple>. That path resolves the target-aware Apple SDK row and injects clang/SDK env internally. Direct soldr cargo zigbuild --target *-apple-darwin is a legacy/diagnostic path, not the default macOS recipe.

x86_64-apple-darwin is supported for local, CI, and release cross-builds. Official Intel macOS archives and PyPI wheels are built on Linux through this blessed path and smoke-tested on macos-15-intel before publication.

Recipe

# Pin the cross targets in rust-toolchain.toml (same shape as Section 1):
#   [toolchain]
#   targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "aarch64-apple-darwin"]
#
# Linux target: soldr selects and prepares the managed toolchain.
soldr prepare --target x86_64-unknown-linux-gnu
soldr build --target x86_64-unknown-linux-gnu --release -p soldr-cli

# musl target: the same managed lifecycle, with a static musl CRT.
soldr prepare --target x86_64-unknown-linux-musl
soldr build --target x86_64-unknown-linux-musl --release -p soldr-cli

# macOS targets: soldr build is the blessed path. `prepare` is optional and
# useful only when a later external/legacy command needs SDKROOT exported.
soldr prepare --target x86_64-apple-darwin
soldr build --target x86_64-apple-darwin --release -p soldr-cli
soldr build --target aarch64-apple-darwin --release -p soldr-cli

What soldr handles automatically

  • Catalogue-backed GCC/binutils/glibc-2.17 sysroot provisioning for GNU Linux targets.
  • Catalogue-backed GCC/binutils/musl sysroot provisioning for canonical musl Linux targets, including startup objects and static runtime libraries.
  • Apple SDK fetch for *-apple-darwin targets. Auto shape maps x86_64-apple-darwin to darwin-x86_64 and aarch64-apple-darwin to darwin-aarch64; soldr build applies the SDK env internally.

Apple SDK version + shape (soldr-toolchain#14)

The Apple SDK soldr fetches is pinned per-target via two env vars (both optional):

Env varValuesDefaultEffect
SOLDR_APPLE_SDK_VERSION11.3, 13.3, 14.5, 15.214.5Which macOS SDK to vendor (catalogue row selection).
SOLDR_APPLE_SDK_SHAPEuniversal2, thin-x86_64, thin-aarch64, autoautoWhether to fetch the fat universal2 artifact or a lipo-thinned per-arch slice.

auto (the default) picks the thin variant matching the target triple's arch when cross-compiling for one Apple arch, falling back to universal2 otherwise. Examples:

# Project targets only Apple Silicon: fetch the thin SDK slice
$env:SOLDR_APPLE_SDK_VERSION = "14.5"
$env:SOLDR_APPLE_SDK_SHAPE   = "thin-aarch64"
soldr build --target aarch64-apple-darwin --release -p soldr-cli

# Project targets both Apple archs from one cache: one fat artifact
$env:SOLDR_APPLE_SDK_SHAPE = "universal2"
soldr build --target x86_64-apple-darwin  --release -p soldr-cli
soldr build --target aarch64-apple-darwin --release -p soldr-cli

The available (version, shape) rows live in the soldr-toolchain catalogue at https://zackees.github.io/soldr-toolchain/catalogue.v1.json under the URL pattern /apple-sdk/<version>/<shape-slug>/. The historical 11.3 row keeps its legacy /apple-sdk/MacOSX11.3/darwin-universal2/ layout; modern rows use the bare version plus shape slug.

Local executable-only symbol check

After installing soldr and an LLVM containing llvm-nm and llvm-symbolizer, run the self-cleaning probe from the repository root:

uv run --no-project python bench/test_darwin_symbols.py
DARWIN_TARGETS=x86_64-apple-darwin,aarch64-apple-darwin \
  uv run --no-project python bench/test_darwin_symbols.py

The probe builds a temporary crate with line-tables-only plus packed debuginfo through soldr build, removes every generated dSYM/object sidecar, and verifies that llvm-symbolizer still maps symbol_probe to main.rs in the Mach-O executable. Set LLVM_NM, LLVM_SYMBOLIZER, or SOLDR_LLVM_DIR when LLVM is not already on PATH.

CI

The Linux cross-build workflow .github/workflows/_ci-cross-build-linux.yml proves the MSVC targets from a Linux host, and the matching e2e-windows-x64 / e2e-windows-arm64 jobs in ci.yml execute the produced archives on native Windows runners.

The Windows GNU target has no CI coverage. A dedicated windows-gnu-mingw-validation.yml workflow used to validate it on a native windows-2025 runner, but it path-triggered on nearly every code PR and did two full release builds to exercise a target that is not in ci/canonical-targets.json and is never shipped — removed in soldr#1982. The managed MinGW path below is still supported and still works; it is simply verified by hand rather than on every PR. Re-add a scheduled-only workflow if Windows GNU regressions start reaching users.

Windows GNU is intentionally handled by the managed MinGW path in Section 1, not by the Linux-hosted zigbuild flow.


2. Linux → Windows MSVC via blessed soldr build

The blessed soldr build --target <triple> path provisions the catalogued Microsoft CRT/Windows SDK inputs and sets up the link step so you can produce MSVC-ABI binaries from a Linux host. soldr cargo xwin remains an explicit legacy passthrough for projects that need the historical cargo-xwin behavior.

rust-toolchain.toml

[toolchain]
channel = "1.95.0"
targets = ["x86_64-pc-windows-msvc"]

[soldr.plugins]
cargo-xwin = { version = "0.18", locked = true }

Bootstrap and build

soldr prepare --target x86_64-pc-windows-msvc
soldr build --release --target x86_64-pc-windows-msvc

Legacy fallback (explicitly bypasses the blessed surface):

soldr cargo xwin build --release --target x86_64-pc-windows-msvc

MSVC EULA

cargo-xwin auto-downloads the MSVC CRT and Windows SDK from Microsoft's servers on first run. By using it, you accept Microsoft's MSVC license implicitly. soldr does not display the EULA; if that matters for your distribution, read upstream's license discussion before adopting the recipe.

Useful env vars (cargo-xwin)

VarPurpose
XWIN_ARCHTarget architecture set to download (x86_64, aarch64, x86)
XWIN_CACHE_DIRWhere to cache the downloaded CRT/SDK
XWIN_INCLUDE_ATLInclude ATL headers (off by default)
XWIN_VARIANTPick a specific SDK variant when multiple match

See rust-cross/cargo-xwin for the complete and current list.


3. Pinned host triples per project (current state)

Today, declaring a cross target involves two rust-toolchain.toml blocks:

  1. [toolchain].targets — tells soldr toolchain prepare to rustup target add the rust-std for that triple.
  2. [soldr.plugins] — tells soldr toolchain prepare to install the cargo extension that drives the cross link (cargo-zigbuild, cargo-xwin, etc.).

Putting both in rust-toolchain.toml means a fresh clone needs only:

soldr toolchain prepare

…to be ready to cross-compile, with the exact same versions as every other developer on the project.

Future enhancement (out of scope for #329)

A unified [soldr.cross-targets] block could collapse the two declarations into one — soldr toolchain prepare would derive both the rust-std install and the matching cargo-extension install from a single entry. That work is intentionally not part of this exploration; if you need it, file a follow-up issue referencing #329.


3.5. ring + aarch64-pc-windows-msvc requires soldr build

Crates depending on ring 0.17.x (rustls, reqwest's TLS, …) include hand-written ARM assembly at pregenerated/sha256-armv8-win64.S etc. ring's build.rs hardcodes c.compiler("clang") for windows-msvc and shells out to clang directly. On aarch64-pc-windows-msvc plain clang rejects this — soldr installs a multicall clang shim (hardlink/copy of soldr) that intercepts the call and re-execs clang-cl with the same argv.

Use the blessed surface: soldr build --target aarch64-pc-windows-msvc (soldr#1012, #882). It installs the shim at ~/.soldr/bin/clang-shim/ ahead of system clang on PATH and uses the managed MSVC SDK cache when that target's cache row is available. The managed ARM64 cache row is now available, so soldr build uses the same catalogue-backed SDK/linker path for aarch64-pc-windows-msvc; the explicit legacy cargo-xwin command remains available as a diagnostic fallback.

Direct cargo-xwin path is unsupported

Bypassing soldr build (e.g. inside messense/cargo-xwin:0.23.0 docker, or a workflow that doesn't go through soldr) means the shim isn't on PATH and the build fails:

error occurred in cc-rs: command did not execute successfully
LC_ALL=C clang ... --target=aarch64-pc-windows-msvc ...
  -c ring-0.17.14/pregenerated/sha256-armv8-win64.S

To use the shim outside soldr-managed shells: install soldr (pip install soldr / npm install -g @zackees/soldr), then run soldr build --target aarch64-pc-windows-msvc --help once to trigger shim install. After that, add $HOME/.soldr/bin/clang-shim to PATH manually and any downstream tool's clang invocation resolves to the shim.

Why not upstream?

Upstream-able in principle, but ring's c.compiler("clang") is intentional and cargo-xwin's CC_*=clang-cl gets overridden by cc-rs's compiler_family() probe. The shim is the surgical fix that lives in soldr's toolchain story. See soldr#886.


4. What soldr deliberately does NOT do

The #329 exploration is explicit about the lines soldr will not cross:

  • No silent RUSTFLAGS injection. soldr does not append linker flags or -C link-arg=... based on guesswork about your target.
  • No hidden CC / CXX / linker mutation. soldr prepare may export target-scoped env for a supported target, such as managed MinGW-w64 GCC for x86_64-pc-windows-gnu, but only on an explicit --target.
  • No system-package installs (mingw-w64, binutils-mingw-w64-x86-64, etc.) from inside soldr. Managed toolchains are downloaded into soldr's cache instead of being installed into the host OS.

For the CI side, zackees/setup-soldr takes liberties that soldr itself will not — including a cross-targets: input that can install the system bits on a GitHub-hosted runner.


5. Status of issue #329 sub-items

Sub-itemStatus
1. Windows GNU managed MinGW pathWorks via soldr prepare / soldr build on Windows x64 hosts (Section 1).
2. Documented xwin recipeThis file (Section 2).
3. Pinned host triples per projectWorks via [toolchain].targets + [soldr.plugins] (Section 3). A unified [soldr.cross-targets] block is deferred.

See also

  • README.md — "Native vs cross targets" covers same-ABI cross targets that only need a rust-std install.
  • docs/API.md — full CLI reference, including soldr toolchain prepare.
  • CLAUDE.md[soldr.plugins] manifest example and contributor guidance.