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 want | Use |
|---|---|
| Any canonical target | soldr build --target <alias-or-triple> |
| Windows x64 → Windows GNU | managed MinGW-w64 GCC + GNU syslibs (Section 1) |
| Linux → Windows MSVC | soldr build (Section 2) |
| Windows/Linux -> Linux | soldr build --target <linux-triple> |
| Windows/Linux -> Mac | soldr 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.
| Alias | Rust target | CI validation | Release |
|---|---|---|---|
win-x64 | x86_64-pc-windows-msvc | Cross-build + native run | Shipped |
win-arm64 | aarch64-pc-windows-msvc | Cross-build + native run | Shipped |
mac-x64 | x86_64-apple-darwin | Cross-build + native run | Shipped |
mac-arm64 | aarch64-apple-darwin | Cross-build + native run | Shipped |
linux-x64 | x86_64-unknown-linux-gnu | Native build + run | Shipped |
linux-arm64 | aarch64-unknown-linux-gnu | Cross-build + native run | Shipped |
linux-x64-musl | x86_64-unknown-linux-musl | Cross-build only (host-identical runner) | Shipped |
linux-arm64-musl | aarch64-unknown-linux-musl | Cross-build + native run | Shipped |
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.17selects 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. Neitherzig,cargo-zigbuild, norziglangis on this blessed GNU path. - musl Linux targets use separate catalogue-backed GCC/binutils/musl static
CRT bundles for
x86_64-unknown-linux-muslandaarch64-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, orziglang.SOLDR_USE_LEGACY_ZIGBUILDis 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. Directsoldr cargo zigbuild --target *-apple-darwinis 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-darwintargets. Auto shape mapsx86_64-apple-darwintodarwin-x86_64andaarch64-apple-darwintodarwin-aarch64;soldr buildapplies 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 var | Values | Default | Effect |
|---|---|---|---|
SOLDR_APPLE_SDK_VERSION | 11.3, 13.3, 14.5, 15.2 | 14.5 | Which macOS SDK to vendor (catalogue row selection). |
SOLDR_APPLE_SDK_SHAPE | universal2, thin-x86_64, thin-aarch64, auto | auto | Whether 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)
| Var | Purpose |
|---|---|
XWIN_ARCH | Target architecture set to download (x86_64, aarch64, x86) |
XWIN_CACHE_DIR | Where to cache the downloaded CRT/SDK |
XWIN_INCLUDE_ATL | Include ATL headers (off by default) |
XWIN_VARIANT | Pick 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:
[toolchain].targets— tellssoldr toolchain preparetorustup target addthe rust-std for that triple.[soldr.plugins]— tellssoldr toolchain prepareto 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
RUSTFLAGSinjection. soldr does not append linker flags or-C link-arg=...based on guesswork about your target. - No hidden
CC/CXX/ linker mutation.soldr preparemay export target-scoped env for a supported target, such as managed MinGW-w64 GCC forx86_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-item | Status |
|---|---|
| 1. Windows GNU managed MinGW path | Works via soldr prepare / soldr build on Windows x64 hosts (Section 1). |
2. Documented xwin recipe | This file (Section 2). |
| 3. Pinned host triples per project | Works 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, includingsoldr toolchain prepare.CLAUDE.md—[soldr.plugins]manifest example and contributor guidance.