How to Fix: wasmtime: `cargo test` fails to build when cross compiling

6 min read

Cross-compiling Wasmtime tests breaks for s390x, riscv64, and aarch64 because cargo test tries to do two different jobs at once: build test binaries for the target architecture and also compile or execute host-side test helpers, build scripts, and proc-macros for the machine you are currently using. In the Wasmtime workspace, that split can surface as architecture-specific build failures when the target is not fully supported for running the produced test executables from the host environment.

Understanding the Root Cause

The key detail is that cargo test is not just a compile command. It also prepares a test harness and, in many setups, expects the generated test binaries to be runnable. When you invoke Wasmtime tests for targets like s390x, riscv64, or aarch64 from a different host architecture, Cargo has to coordinate multiple compilation contexts:

  • Host artifacts: build scripts, proc-macros, and tooling crates that must compile for the current machine.
  • Target artifacts: the actual library, integration tests, and test executables intended for the foreign architecture.
  • Runtime assumptions: some tests or dependencies assume the target binary can be linked and, in some workflows, executed.

In a large workspace like Wasmtime, this often fails during cross-compilation of test targets because test-only dependencies, linker configuration, or architecture-specific code paths are stricter than ordinary cargo build. A crate may build successfully for a foreign target, but its test harness may pull in extra code that does not. That is why cargo build --target ... can work while cargo test --target ... fails.

Another common factor is that cross-target test execution is not equivalent to native test execution. If no emulator such as QEMU is configured through Cargo runners, Cargo cannot meaningfully run the produced test binaries. Even before execution, test compilation may fail if the workspace expects target-specific components, native libraries, or linker settings not present in the cross environment.

For Wasmtime specifically, the practical fix is usually to separate compilation validation from execution. If your goal is to verify that code compiles for s390x, riscv64, or aarch64, use build-oriented commands instead of plain cargo test. If you truly need tests, run them under an explicit cross-runner or on native hardware for that architecture.

Step-by-Step Solution

The safest approach is to replace unsupported cross-target test runs with a workflow that matches what Cargo can actually guarantee.

1. Add the target toolchain

rustup target add s390x-unknown-linux-gnu
rustup target add riscv64gc-unknown-linux-gnu
rustup target add aarch64-unknown-linux-gnu

This ensures Rust can emit artifacts for each requested architecture.

2. Use cargo build or cargo check instead of cargo test for cross-compilation validation

cargo check --workspace --target s390x-unknown-linux-gnu
cargo check --workspace --target riscv64gc-unknown-linux-gnu
cargo check --workspace --target aarch64-unknown-linux-gnu

If you need a full code generation pass rather than type-checking only:

cargo build --workspace --target s390x-unknown-linux-gnu
cargo build --workspace --target riscv64gc-unknown-linux-gnu
cargo build --workspace --target aarch64-unknown-linux-gnu

This is the most reliable solution when the issue is specifically that cargo test fails during cross-compilation.

3. If you only need to compile tests, use --no-run

cargo test --workspace --target aarch64-unknown-linux-gnu --no-run

This can help in environments where execution is the problem, but it may still fail if test-only dependencies or harness code do not cross-compile cleanly. If that happens, fall back to cargo check or cargo build for the workspace, or scope testing to crates that are known to support cross-target test builds.

4. Configure the correct linker for each target

If Cargo reports linker failures, add a target-specific configuration file.

[target.s390x-unknown-linux-gnu]
linker = "s390x-linux-gnu-gcc"

[target.riscv64gc-unknown-linux-gnu]
linker = "riscv64-linux-gnu-gcc"

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

Save that in .cargo/config.toml. The exact linker names depend on your distro and installed cross toolchains.

5. Run tests natively or through an emulator when actual execution is required

If your goal is real test execution rather than compile verification, use one of these options:

  • Run CI jobs on native aarch64, s390x, or riscv64 machines.
  • Configure a Cargo runner backed by QEMU.
  • Limit host-side test execution to architectures supported by the current machine.

Example Cargo runner setup:

[target.aarch64-unknown-linux-gnu]
runner = "qemu-aarch64 -L /usr/aarch64-linux-gnu"

[target.s390x-unknown-linux-gnu]
runner = "qemu-s390x -L /usr/s390x-linux-gnu"

Once configured, you can retry:

cargo test --workspace --target aarch64-unknown-linux-gnu

Be aware that emulator-based testing is slower and may still expose target-environment differences.

6. For CI, split compile coverage from test coverage

A robust Wasmtime pipeline usually looks like this:

# Native host tests
cargo test --workspace

# Cross-target compile validation
cargo check --workspace --target s390x-unknown-linux-gnu
cargo check --workspace --target riscv64gc-unknown-linux-gnu
cargo check --workspace --target aarch64-unknown-linux-gnu

This avoids treating cross-compilation and cross-execution as the same problem.

Common Edge Cases

  • Linker not found: Rust target support alone is not enough. You also need the correct cross C toolchain and system linker.
  • Build scripts compile for host, not target: crates with build.rs may behave differently than expected during cross builds if they probe the system or compile native code.
  • Proc-macro confusion: proc-macros always build for the host architecture, which can make mixed host/target failures look misleading.
  • Test-only dependencies fail: integration tests often pull in crates or features that normal library builds do not, so cargo build succeeding does not guarantee cargo test --no-run will succeed.
  • Missing emulator sysroot: QEMU runners often require a valid -L sysroot path matching the target userspace libraries.
  • Architecture-specific assumptions in tests: some tests may assert pointer width, endianness, page size, or JIT behavior that differs across platforms.
  • Workspace-wide failures from one crate: in a large repository like Wasmtime, one unsupported test target in a subcrate can break the entire workspace command.

FAQ

Why does cargo build work but cargo test fail for the same target?

Because cargo test compiles additional test harness, test-only dependencies, and integration test binaries. Those extra artifacts often introduce linker requirements or architecture-specific code paths that a normal build does not touch.

Can I force Cargo to just compile tests without running them?

Yes. Use cargo test --no-run --target <triple>. However, this still builds the test binaries, so it can fail if the test code itself is not cross-compilation-friendly.

What is the best fix for Wasmtime CI?

The most maintainable approach is to run native tests on supported host architectures and use cargo check or cargo build for foreign-target validation. If execution on s390x, riscv64, or aarch64 is required, add dedicated native runners or emulator-backed jobs instead of relying on plain host-side cargo test.

In short, the bug is not just that Wasmtime fails to build tests for these targets; it is that cross-compiling test binaries is a stricter and different workflow than cross-compiling normal Rust code. The fix is to choose the right command for the goal: build/check for portability, test for native or emulated execution.

Leave a Reply

Your email address will not be published. Required fields are marked *