dockerrustmusl

Can I build a multi-platform Docker image containing Rust code with native dependencies targeting musl?


TL;DR: I have a Dockerfile which I want to use to build some Rust code (with native dependencies) into as small as possible of a multi-platform image (for both linux/amd64 and linux/arm64) via the following command:

docker build --platform linux/amd64,linux/arm64 .

When I run this on my M1 MacBook, the linux/arm64 part succeeds but the linux/amd64 part fails, and when I run it on an x86 Linux machine in GitHub Actions, the linux/amd64 part succeeds but the linux/arm64 part fails. I've enabled containerd (and QEMU in CI) so I can build other multi-platform images just fine; this seems specific to building Rust code with native dependencies targeting musl.


I have a Rust binary crate that depends on Wasmtime; here's my Cargo.toml. (Note that if I remove wasmtime from my [dependencies], this error no longer occurs.)

[package]
name = "foo"
edition = "2021"

[dependencies]
wasmtime = "29"

I have this Dockerfile to build it; I'm using musl to build a static binary so I can pass it to the second stage which is built FROM scratch:

FROM rust
ARG TARGETARCH
RUN apt-get update && apt-get install -y musl-tools
WORKDIR /root
COPY . .
RUN rustup target add "$(./target.py $TARGETARCH)"
RUN cargo build --target "$(./target.py $TARGETARCH)"
RUN cp "target/$(./target.py $TARGETARCH)/debug/foo" .

FROM scratch
COPY --from=0 /root/foo /root/foo
ENTRYPOINT ["/root/foo"]

The target.py script just translates a Docker TARGETARCH to a Rust target:

#!/usr/bin/env python3

import sys

targets = {
    "amd64": "x86_64-unknown-linux-musl",
    "arm64": "aarch64-unknown-linux-musl",
}
print(targets[sys.argv[1]])

When I'm just building natively, everything works fine:

docker build . -t foo && docker run --rm foo

But if I attempt to build a multi-platform image, it fails:

docker build --platform linux/amd64 .

For instance, here is the most relevant portion of the error output when building on my M1 MacBook:

error occurred in cc-rs: Command ZERO_AR_DATE="1" "musl-ar" "cq" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/libzstd.a" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/44ff4c55aa9e5133-debug.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/44ff4c55aa9e5133-entropy_common.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/44ff4c55aa9e5133-error_private.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/44ff4c55aa9e5133-fse_decompress.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/44ff4c55aa9e5133-pool.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/44ff4c55aa9e5133-threading.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/44ff4c55aa9e5133-zstd_common.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-fse_compress.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-hist.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-huf_compress.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstd_compress.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstd_compress_literals.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstd_compress_sequences.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstd_compress_superblock.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstd_double_fast.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstd_fast.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstd_lazy.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstd_ldm.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstd_opt.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/fb80479a5fb81f6a-zstdmt_compress.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/88f362f13b0528ed-huf_decompress.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/88f362f13b0528ed-zstd_ddict.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/88f362f13b0528ed-zstd_decompress.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/88f362f13b0528ed-zstd_decompress_block.o" "/root/target/x86_64-unknown-linux-musl/debug/build/zstd-sys-7442e8fce933a8d2/out/7faed3f8272f2313-huf_decompress_amd64.o" with args musl-ar did not execute successfully (status code exit status: 127).

I've also created a GitHub repository containing the full set of files to reproduce this example.


Is it possible to modify my Dockerfile to build this multi-platform image on one machine via the single docker build command I listed at the top (which already works for all my other Dockerfiles)? I know that there are other ways to build cross-platform musl binaries (cross works in this case) but I want to keep my Docker build infrastructure consistent with the rest of my larger project.


Solution

  • TL;DR: add this to your Dockerfile somewhere before the cargo build command:

    ENV TARGET_AR=ar
    

    In this particular case, here is the chain of dependencies to the issue:

    In its build.rs script, the zstd-sys crate makes this chain of calls:

    That last method returns basically the same thing as cc::Build::get_archiver, which is usually just ar. But not always:

    The TARGET_AR environment variable can be used to override cc's default archiver choice. It's not documented anywhere, but is implemented here as part of a generic "{kind}_{var_base}" pattern where in this case kind is "TARGET" and var_base is "AR".

    I've opened issue rust-lang/cc-rs#1399 to see whether the cc maintainers consider this a bug.