0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Windows で Docker と cross を使って ARMv7 musl-libc 向けに Rust をクロスコンパイルする

0
Posted at

Gemini 3.1 Pro を使っても 10 時間くらい溶かしたので、悔しさから備忘録を残しておく。

何がしたかったか

Slint という UI ライブラリで作成した Rust 製のバイナリを、Windows 環境でビルドし、Alpine Linux を導入した Raspberry Pi 2 (ARMv7) で動かしたかった。

このプログラムは依存関係に *-sys 系クレートが多いため、すんなり Windows からクロスコンパイルができない。

もちろん ARMv7 アーキテクチャの性能がいいマシンは持っていない。また Raspberry Pi でビルドするのは「負け」だろうと考えたため、cross を使用するか、Alpine Linux をエミュレーションするしかないと考えられる。
まず Docker のインストールに踏み切ることにした。

Docker のインストールを WSL にする(たぶん任意)

しかし、私は Docker が好きではない。
なるべく Windows に Docker を入れたくなかったため、Docker がパスを通すだけで使えるシングルバイナリならそれで動かすか、WSL に入れようと考えた。
前者も取り組んだが、以降に示しているゴタゴタによって、最終的に WSL になった。

https://cloud-images.ubuntu.com/wsl/releases/noble/current/ から WSL 用の Ubuntu rootfs をダウンロードしてきて、以下のように WSL のディストリビューションとしてインポートする。

docker_cleanup.bat
@echo off
setlocal

set WSL_NAME=DockerEngine
set DIR_ENGINE=G:\Applications\Developers\Docker\WSL
set TAR_ROOTFS=G:\Applications\Developers\Docker\ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz

echo 既存の VM を削除しています...
wsl --unregister %WSL_NAME% >nul 2>&1

echo VMUbuntu を書き込んでいます...
if not exist "%DIR_ENGINE%" (
    mkdir "%DIR_ENGINE%"
)
wsl --import %WSL_NAME% "%DIR_ENGINE%" "%TAR_ROOTFS%"
if %ERRORLEVEL% neq 0 (
    echo [ERROR] インポートに失敗しました。パスやファイル名を確認してください。
    pause > nul
    exit /b %ERRORLEVEL%
)

echo VMDocker をインストールしています...
:: 1. 依存関係の解決と Docker 公式の GPG キーを配置
wsl -d %WSL_NAME% -u root -- bash -c "apt-get update && apt-get install -y ca-certificates curl && install -m 0755 -d /etc/apt/keyrings && curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc && chmod a+r /etc/apt/keyrings/docker.asc"
:: 2. Docker 公式リポジトリ (Ubuntu Noble 用) を APT ソースリストに追加
wsl -d %WSL_NAME% -u root -- bash -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable' > /etc/apt/sources.list.d/docker.list"
:: 3. 公式の Docker Engine (docker-ce) と buildx プラグインをインストール
wsl -d %WSL_NAME% -u root -- bash -c "apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin"
:: 4. systemd の有効化
wsl -d %WSL_NAME% -u root -- bash -c "echo '[boot]' > /etc/wsl.conf && echo 'systemd=true' >> /etc/wsl.conf"

echo VM を停止しています...
wsl --terminate %WSL_NAME%

echo.
echo セットアップが完了しました。
pause

「ディストリビューション」ではなく「VM」としているのは手癖。
distro って聞くと Ubuntu, Debian, ... のように聞こえてしまう……気がする。

これで wsl -d DockerEngine -- docker を叩くことで Docker 自体は呼ぶことができるが、cross が Docker を実行できなくなってしまうため、ラッパーを Rust で(Gemini 3.1 Pro が)以下のように書いた。

docker.rs
// WSL の指定されたコンテナの中で動く dockerd を呼び出す Rust ツール
// コンパイル: rustc docker.rs -o docker.exe

use std::env;
use std::process::{Command, exit};

fn main() {
    // 1. WSL Docker Engine の起動状態チェックと遅延起動
    let check = Command::new("wsl")
        .args(["--list", "--running"])
        .output()
        .expect("Failed to execute wsl");

    let running = String::from_utf8_lossy(&check.stdout);
    if !running.contains("DockerEngine") {
        let _ = Command::new("wsl")
            .args(["-d", "DockerEngine", "-u", "root", "systemctl", "start", "docker"])
            .status();
        std::thread::sleep(std::time::Duration::from_secs(2));
    }

    // 2. 引数の構築と安全なパス変換
    let mut wsl_args: Vec<String> = vec!["-d".into(), "DockerEngine".into(), "--".into(), "env".into()];

    // Windows側の特定の環境変数をWSL側へ引き継ぐ
    for (key, value) in std::env::vars() {
        if key.starts_with("CROSS_") || key.starts_with("DOCKER_") {
            wsl_args.push(format!("{}={}", key, value));
        }
    }

    // 最後に docker コマンドを積む
    wsl_args.push("docker".into());

    for arg in env::args().skip(1) {
        let mut processed = arg.clone();

        // 変換条件: 引数が "Z:\" や "C:/" のようにドライブレターから始まる場合のみ
        if processed.len() >= 3 {
            let chars: Vec<char> = processed.chars().collect();
            if chars[0].is_ascii_alphabetic() && chars[1] == ':' && (chars[2] == '\\' || chars[2] == '/') {
                let drive = chars[0].to_ascii_lowercase();

                // "Z:\" を "/mnt/z/" に置換
                processed = format!("/mnt/{}/{}", drive, &chars[3..].iter().collect::<String>());

                // パス内のバックスラッシュをスラッシュに統一
                processed = processed.replace('\\', "/");
            }
        }
        wsl_args.push(processed);
    }

    // 3. WSL 内の docker を実行 (引数は配列として直接OS API経由で渡されるためクォートは絶対壊れない)
    let status = Command::new("wsl")
        .args(&wsl_args)
        .status()
        .expect("Failed to execute wsl docker");

    exit(status.code().unwrap_or(1));
}

最初は PowerShell とかで書いていたが、cross が吐き出す "' やエスケープもりもりのコマンドを解釈すると無傷で引き継ぐのがかなり厳しくなってしまったため、Rust で書くことになった(1敗)。

ここで、環境変数とかを引き継いでおくほうが後々よかった(1敗)。
また、無駄に stdout に書き込むと cross が誤反応する(1敗)ので、ラッパーは何も出力しないほうがいい。

これを rustc docker.rs -o docker.exe としてパスを通しておく。

image.png

最終的に、WSL のディストリビューション用の .vdhx ファイル (5GB 程度) 一つでまとまるようになってうれしい。
これは私がやりたかっただけなので、Docker が普通に使える人はやらなくてもいいと思う。

試行錯誤

cross を使い armv7-unknown-linux-gnueabihf でコンパイルする(失敗)

Cross.toml
[target.armv7-unknown-linux-gnueabihf]
dockerfile = "./Dockerfile.cross"
Dockerfile.cross
FROM ghcr.io/cross-rs/armv7-unknown-linux-gnueabihf:edge

# クロスコンパイル用の環境変数を明示的に設定
ENV PKG_CONFIG_ALLOW_CROSS=1

# コンテナ内部の OS に armhf アーキテクチャとライブラリを追加
RUN dpkg --add-architecture armhf && \
    apt-get update && \
    apt-get install -y libfontconfig1-dev:armhf pkg-config

これでビルドを実行する。

cross build --target armv7-unknown-linux-gnueabihf --release

すんなりビルドが通ってしまった。
しかし実行してみると謎にエラーを吐く。

raspberry-pi:/tmp# ./APP_NAME
-sh: ./APP_NAME: not found

どうやら Docker のイメージが glibc 向けにコンパイルされたものらしく、Alpine Linux で採用されている musl-libc に合わないらしい。
gnueabihfmusleabihf に変更してビルドしてみる。

cross を使い armv7-unknown-linux-musleabihf でコンパイルする(失敗)

Cross.toml
[target.armv7-unknown-linux-musleabihf]
dockerfile = "./Dockerfile.cross"
Dockerfile.cross
FROM ghcr.io/cross-rs/armv7-unknown-linux-musleabihf:edge

# クロスコンパイル用の環境変数を明示的に設定
ENV PKG_CONFIG_ALLOW_CROSS=1

# コンテナ内部の OS に armhf アーキテクチャとライブラリを追加
RUN dpkg --add-architecture armhf && \
    apt-get update && \
    apt-get install -y libfontconfig1-dev:armhf pkg-config

先ほどのものを armv7-unknown-linux-musleabihf に変えただけ。
これで cross build を実行。

cross build --target armv7-unknown-linux-musleabihf --release

しかしまだエラーが出る。

error: failed to run custom build command for `yeslogic-fontconfig-sys v6.0.0`

Caused by:
  process didn't exit successfully: `/target/release/build/yeslogic-fontconfig-sys-12f97dec0e56c92a/build-script-build` (exit status: 101)

  --- stdout
  cargo:rerun-if-env-changed=RUST_FONTCONFIG_DLOPEN
  ... (略) ...

  --- stderr
  thread 'main' (1035) panicked at /mnt/z/Developers/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/yeslogic-fontconfig-sys-6.0.0/build.rs:8:48:
  called `Result::unwrap()` on an `Err` value: "\npkg-config exited with status code 1\n> PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 pkg-config --libs --cflags fontconfig\n\nThe system library `fontconfig` required by crate `yeslogic-fontconfig-sys` was not found.\nThe file `fontconfig.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.\nThe PKG_CONFIG_PATH environment variable is not set.\n\nHINT: if you have installed the library, try setting PKG_CONFIG_PATH to the directory containing `fontconfig.pc`.\n"
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

ライブラリが足りないと怒られている。
Gemini にそんなことはないのではと聞くと、先ほど Dockerfile に書いていた libfontconfig1-dev:armhf は glibc 向けにコンパイルされているからとのこと。

これ以上 cross に付き合いたくないと思ったため、次の方法を実施した。

QEMU + Alpine コンテナでビルドする(多分失敗)

この時点で 3 時間ほど費やしており飽きてきたため、いっそのこと QEMU でエミュレーションして動かすことにした。
Dockerfile なども刷新している。

Dockerfile
FROM alpine:latest

RUN apk add --no-cache \
    rust \
    cargo \
    pkgconf \
    fontconfig-dev \
    freetype-dev \
    gcc \
    g++ \
    musl-dev \
    make \
    linux-headers

WORKDIR /app
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release && \
    mkdir -p /app/out && \
    cp /app/target/release/APP_NAME /app/out/APP_NAME

これを用意した後、まず QEMU エミュレーションのセットアップをする。

docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

multiarch/qemu-user-static を実行すると、Docker が動いているカーネルの binfmt_misc 機能を利用して、別のアーキテクチャ向けのバイナリでも QEMU エミュレーションした状態で動かしてくれる。
なお、wsl --shutdown したらこの設定は消えてしまうので、WSL の起動ごとにやる必要がある。exec format error などのエラーが出てきたら、この設定をし忘れているのが原因(1敗)。

これで、Docker 上で完全に ARMv7 の Alpine Linux が動いていることになった。
以下のビルドコマンドを実行する。

docker build --platform linux/arm/v7 -t APP_NAME-builder -f Dockerfile .

# バイナリの取り出し
# docker create --name temp-container APP_NAME-builder
# docker cp temp-container:/app/out/APP_NAME ./APP_NAME
# docker rm temp-container

勝ったな。風呂入ってくる。
……なんか遅いな。

[+] Building 1483.5s (8/9)                                                                               docker:default
    (不要なログのため削除)
 => [stage-0 4/5] COPY . .                                                                                        16.2s
 => [stage-0 5/5] RUN --mount=type=cache,target=/usr/local/cargo/registry     --mount=type=cache,target=/app/t  1399.1s
 => => # rustc-LLVM ERROR: out of memory
 => => # Buffer allocation failed

死んだ。依存関係のビルドにはすべて成功しているが、最終的にコンパイルしたい自分のクレートだけ死んだ。

Windows マシンはメモリ 32GB、WSL 標準設定で使用しているため WSL には 16GB のメモリを積んでいる。
また、autoMemoryReclaimdropCache になっている。

ここで粘ってもよかったのだが、何しろこのビルドに 1 時間かかる。ログには 1483.5s と書かれているが、長い時で約 51 分を記録した。
さすがに何か別の方法があるだろうと考えて、ひとまず完全なエミュレーションはあきらめることにした。

x86_64 の apk で ARMv7 用ライブラリをインストールし、cross でビルド(成功)

イメージ名が違う(1敗)だとか、--initdb がなくて apk add が動かない(1敗)だとか、/ ではなく /sysroot のリポジトリを参照する(1敗)だとかいろいろあったが、何とか動いた。

注意点としてコメントにも書いてあるが、インストールスクリプトが走ると、スクリプトのアーキテクチャが合わず死ぬ(1敗)。
あくまで apk add は Windows ホスト機の x86_64 で動いており、--arch armv7 で無理やり armv7 用のライブラリを入れているだけ。

Cross.toml
[target.armv7-unknown-linux-musleabihf]
dockerfile = "./Dockerfile.cross"
Dockerfile.cross
# --- Stage 1: sysroot の構築 ---
FROM alpine AS sysroot

# x86_64 の apk を使用して、armv7 のパッケージを /sysroot に展開する
# [!] リポジトリは alpine イメージの初期の鍵と異なるので、--allow-untrusted を指定する
# [!] インストールスクリプトが走らないよう、--no-scripts を指定する
RUN apk add --no-cache \
    --root /sysroot \
    --initdb \
    --arch armv7 \
    --allow-untrusted \
    --no-scripts \
    --repository https://dl-cdn.alpinelinux.org/alpine/v3.23/main \
    --repository https://dl-cdn.alpinelinux.org/alpine/v3.23/community \
    fontconfig-dev \
    eudev-dev \
    libseat-dev \
    freetype-dev \
    expat-dev \
    libxkbcommon-dev \
    libinput-dev \
    mesa-dev

# --- Stage 2: cross 環境への統合 ---
FROM ghcr.io/cross-rs/armv7-unknown-linux-musleabihf:edge

COPY --from=sysroot /sysroot/lib /sysroot/lib
COPY --from=sysroot /sysroot/usr /sysroot/usr

# クロスコンパイル時に pkg-config が動作するよう許可
ENV PKG_CONFIG_ALLOW_CROSS=1
# pkg-config がクロス環境 (Ubuntu) のものではなく、/sysroot 以下の情報を参照するように設定
ENV PKG_CONFIG_DIR=
ENV PKG_CONFIG_LIBDIR=/sysroot/usr/lib/pkgconfig:/sysroot/lib/pkgconfig
ENV PKG_CONFIG_SYSROOT_DIR=/sysroot
# bindgen などを使用する他の -sys クレートが存在する場合に備えた設定
ENV BINDGEN_EXTRA_CLANG_ARGS="--sysroot=/sysroot"

なお、Mesa (libgbm) は実行時にハードウェア固有の GPU ドライバを読み込む挙動をするらしく、静的リンクは諦める必要がある。

.cargo/config.toml
[target.armv7-unknown-linux-musleabihf]
rustflags = [
    "-C", "target-feature=-crt-static",
    "-C", "link-arg=-Wl,-rpath-link=/sysroot/usr/lib",
    "-C", "link-arg=-Wl,-rpath-link=/sysroot/lib"
]

-path-link=/sysroot/... を指定するのもポイント(1敗)。

ようやく以下を叩くことで、成功。

cross build --target armv7-unknown-linux-musleabihf --release

実行側でやること

こういうのが出ることがある。

Error: Could not initialize any renderer for LinuxKMS backend.
Error from FemtoVG renderer: Error creating EGL display: not found
Error from Software renderer: Could not open any legacy framebuffers.
Error using /dev/fb0: Error opening device /dev/fb0: Resource temporarily unavailable
Error using /dev/fb1: Error opening device /dev/fb1: Resource temporarily unavailable
Error using /dev/fb2: Error opening device /dev/fb2: Resource temporarily unavailable
Error using /dev/fb3: Error opening device /dev/fb3: Resource temporarily unavailable
Error using /dev/fb4: Error opening device /dev/fb4: Resource temporarily unavailable
Error using /dev/fb5: Error opening device /dev/fb5: Resource temporarily unavailable
Error using /dev/fb6: Error opening device /dev/fb6: Resource temporarily unavailable
Error using /dev/fb7: Error opening device /dev/fb7: Resource temporarily unavailable
Error using /dev/fb8: Error opening device /dev/fb8: Resource temporarily unavailable
Error using /dev/fb9: Error opening device /dev/fb9: Resource temporarily unavailable
No renderers configured.

Alpine Linux では以下を入れておく必要がある(1敗)。

apk add libseat libxkbcommon eudev-libs libinput fontconfig seatd mesa-gbm mesa-egl mesa-gles mesa-dri-gallium udev

また、Raspberry Pi においての話だが、/boot/config.txt あるいはそれ相当の設定ファイルに dtoverlay=vc4-kms-v3d を書き込んでおく必要もある(1敗)。

あと、実行環境が root だったから意識してなかったが、一般ユーザーなら video グループに入れる必要があるかもしれない、という情報も見かけた。

完走した感想

もうやりたくない

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?