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 のディストリビューションとしてインポートする。
@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 VM に Ubuntu を書き込んでいます...
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 VM に Docker をインストールしています...
:: 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 が)以下のように書いた。
// 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 としてパスを通しておく。
最終的に、WSL のディストリビューション用の .vdhx ファイル (5GB 程度) 一つでまとまるようになってうれしい。
これは私がやりたかっただけなので、Docker が普通に使える人はやらなくてもいいと思う。
試行錯誤
cross を使い armv7-unknown-linux-gnueabihf でコンパイルする(失敗)
[target.armv7-unknown-linux-gnueabihf]
dockerfile = "./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 に合わないらしい。
gnueabihf を musleabihf に変更してビルドしてみる。
cross を使い armv7-unknown-linux-musleabihf でコンパイルする(失敗)
[target.armv7-unknown-linux-musleabihf]
dockerfile = "./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 なども刷新している。
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 のメモリを積んでいる。
また、autoMemoryReclaim は dropCache になっている。
ここで粘ってもよかったのだが、何しろこのビルドに 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 用のライブラリを入れているだけ。
[target.armv7-unknown-linux-musleabihf]
dockerfile = "./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 ドライバを読み込む挙動をするらしく、静的リンクは諦める必要がある。
[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 グループに入れる必要があるかもしれない、という情報も見かけた。
完走した感想
もうやりたくない
