Rustプログラムのクロスコンパイル
大苦戦したのでアプローチを整理しておく。まったくもって決定版ではない。
ホストやゲストとか言ってるとだんだんわけがわからなくなってくるので、ここではあえて具体的に、「x86_64のPCで、ARM環境(Raspberry Pi)向けにコンパイルする」想定でメモする(この組み合わせはよくあると思う)。手順そのものはx86/ARM特有のことではないので、適宜MIPSやRISC-Vに読み替えればよい。
OpenSSLを使った成功例は [[ priv/chari-clientのビルド ]]を参照。
ビルドの環境とtargetの環境はなるべく近づけたほうがよい(libfoo:armhf
などをインストールするときは、たいていビルド環境のdistroのバージョンに合うのが入るので)
Dockerを使う
cross
まず試すべきは rust-embedded/cross
(以下cross)。インストールしてから、cargo
をcross
で置き換えると、あらかじめARMのツールチェインが入ったDockerコンテナでビルドしてくれる。外部ライブラリに依存しなければこれでうまくいくことも多い。
$ cross build --target=arm-none-linux-gnueabihf
しかしDockerで動かす都合上、
- 毎回依存するcrateをすべてビルドし直す
- 何かエラーが起こってもコンテナの中に入るのに手間がかかる
./configure
が出力したconfig.log
を見たいときなど-v
や-vv
をつけると、crossが実行しているコマンドを見ることができる。Dockerを実行しているコマンドをコピーしてきて最後のcargo
の呼び出しを削るとシェルに入れるので、そこで改めてcargo b
を実行すれば終了後も探索ができる。
といった不便がある。ベースとなるイメージはUbuntu 18.04 (2021/11/07時点) なので、Dockerイメージを拡張してaptで依存ライブラリを入れてもちょっと古いのが入る。
Dockerイメージを作る
マイナーなdistroを使っているとクロスコンパイルまで情報がないことがある。Dockerを使ってapt
の恩恵に授かるのもよい。rust:latest
はUbuntuベース(2021/11/07時点でbullseye) 。
FROM rust:latest
WORKDIR /build
RUN dpkg --add-architecture armhf && \
apt-get update
RUN \
rustup target add arm-unknown-linux-gnueabihf
RUN apt-get install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf cmake
RUN apt-get install -y libfreetype6-dev:armhf libfontconfig1-dev:armhf
ENV CC=arm-linux-gnueabihf-gcc
ENV CXX=arm-linux-gnueabihf-g++
ENV AR=arm-linux-gnueabihf-ar
ENV TARGET=arm-linux-gnueabihf-
ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
ENV PKG_CONFIG_ALLOW_CROSS=1
ENV PKG_CONFIG_LIBDIR=/usr/lib/arm-linux-gnueabihf/pkgconfig
CMD ["cargo", "build", "--target", "arm-unknown-linux-gnueabihf", "--release"]
ビルドと実行:
$ docker build -t hogehoge:latest ./docker
$ docker run -it -v "$PWD":/build hogehoge:latest
自前のツールチェインを使う
Dockerが使えない場合やcross
でうまくいかない場合にはcargo
でやる。この場合はgcc
などツールチェインを自分で用意する。
- (初めてのコンパイルなら)rustupでアーキテクチャを追加する
(project)/.cargo/config
に追記する$ cargo build --target=arm-unknown-linux-gnueabihf
SSL
まずはどのパッケージがSSLに依存しているか確かめる。実際にはクロスコンパイルをかけてから依存していることに気づくケースも多いだろう。
可能であれば、OpenSSLではなくrustls
を使うのがラク。rustls
はOpenSSL同様の機能をRustで実装していて、ライブラリをクロスコンパイルする必要がないからだ(古いプロトコルの実装を省くなどの差異はある)。したがって、そのcrateでrustlsが使える実装になっていないか確認するのがよい。間接的に依存している場合でも、SSLを利用しているcrateが間接的な指定に対応していることもある。
例としてelasticsearch
crateを挙げる。このcrateはreqwest
に依存している。reqwest
はfeatureでnative-tls
とrustls-tls
を切り替えられる。そこで、elasticsearch
からこれを間接的に選択できるようにするためissueが立てられ、実装された:
- native-tls (enabled by default): Enables TLS functionality provided by native-tls.
passes through to reqwest/native-tls and is set as a default feature.
- rustls-tls: Enables TLS functionality provided by rustls.
passes through to reqwest/rustls-tls
rustls-tls
を有効にするだけでは、デフォルトで有効なnative-tls
が残ってしまい無駄になる。現状、デフォルトで有効になっているfeatureを明示的に無効化する(opt outする)にはdefault-features = false
を指定する方法しかない(Cargo issue#3126)。よってこんな感じで指定する:
elasticsearch = { version = "7.14.0-alpha.1", default-features = false, features = ["rustls-tls"] }
OpenSSLしか使えない場合はOpenSSLの節を参照。
ライブラリ
hyper
- https://github.com/hyperium/hyper/issues/2434
CARGO_TARGET_<triple>_LINKER
がCC, AR
とは別に必要(*-gcc
を指定すればいい)。CC, AR
は依存ライブラリ(makeされるもの)に影響する。CARGO_*
は、cargoが最終的にRustのコードをリンクするときに影響する。
servo-fontconfig-sys
build.rs
,makefile.cargo
を見るconfig.log
を見る- RPiのfontconfigが2.13.1, servo-fontconfig-sysに同梱されていたのが2.11.1
- force_system_lib featureを使うと、システムのを(pkg-config経由で)使えない場合はエラーになる
- crossのDockerイメージが古すぎて、fontconfigがたしか2.11か2.12のをリンクしてしまった
- 静的リンクするとラクなことも多いのだが、設定ファイルが絡む場合はそういうわけにもいかない
PyO3
PyO3のクロスコンパイルを参照。いつかリベンジしたい。
serialport
serialport
crateはlibudevを使う。これのビルドが面倒なら、libudevを使わないように設定することができる(StackOverflow)。
serialport = { version = "*", default-features = false }
OpenSSL
OpenSSLしか使えない場合、次の手段でARM向けのOpenSSLを導入する。
cross
のdocker imageを拡張してaptでlibssl:armhf
などを導入する- Dockerを使わずaptなどでlibsslを導入する
- 手元でOpenSSLをクロスコンパイルする
- OpenSSLのソースコードを取ってきて単体でビルド(←いちばんラク?)
- Buildrootを使う
aptなどパッケージマネージャを使う方法では、ビルドが楽にできたりそもそもビルド済みだったりする。他のx86向けライブラリとごっちゃになったり、インストールディレクトリがヘンなところになったりするので、OpenSSL単体で手動でビルドするほうが楽かもしれない。Buildrootを使うほどでもなさそう。
単体でビルドするのはRustのRaspberry Pi向けクロスコンパイル、OpenSSLの対策の手法を真似した。ここではrust:1.44
イメージの上に構築しているが、自分はcross
のイメージの上で動かした。
openssl
crateのビルド中のエラーメッセージは改行が\n
になってしまって見づらいい。crateの出力するエラーメッセージは改行が崩れていなくてパッと目につくのだが、たまに的外れなことを書いてある。そのちょっと上にあるld
かgcc
が出力した崩れたエラーメッセージを読む。
「OPENSSL_DIR
を設定しろ!」といったことを言われる。そういった環境変数の情報はcrateのドキュメントにまとまっている。
libc
glibcのバージョン
RPiのはちょっと古いので、glibcのバージョンが噛み合わなくて困る。その場合には、
- muslで試してみる
- RPiのglibcを更新する
手段がある。クロスコンパイル時に古いglibcについても互換性を持たせることはできないと思われる。
RPiのglibcを更新するとはいっても、ただapt update
するだけでは十分に新しくならないことがある。その場合はtesting
リポジトリを使ってもいいが、いろいろ不具合が生じかねない。可能ならdistroごとアップグレードしてしまうのがよい。それでもダメならもう自前でglibcをビルドするしかない。そこまで試したことはない。
要検証:なんかビルドしたバイナリによってGLIBC_2.32
を要求してたりGLIBC_2.28
で事足りてたりする。何が違う?
musl
muslにするには、tripleをたとえばarm-unknown-linux-musleabihf
にする。ただしmuslに対応したツールチェインは通常distroからは配布されていない。musl-gcc
を入れてREALCC
とかを設定すると、glibc向けのいろいろをmuslに適用させて(?)使うことができる。Adventures in Rust and Cross Compilation for the Raspberry Piが参考になった(Compile for everything! の節を見よう)。
結局こんなコマンドを打ったがコンパイルには失敗した。
PKG_CONFIG_SYSROOT_DIR=/path/to/sysroot
PKG_CONFIG_DIR=/path/to/sysroot/usr/lib/pkgconfig
OPENSSL_DIR=/path/to/sysroot/usr
OPENSSL_INCLUDE_DIR=/path/to/sysroot/usr/include
REALGCC=arm-none-linux-gnueabihf-gcc
TARGET_CC=musl-gcc
cargo b --target arm-unknown-linux-musleabihf
musl.ccにクロスコンパイル用のprebuilt toolchainがあるのでこれを使うのもよい。たとえばx86_64でarm-unknown-linux-musleabihf
向けにコンパイルするならば、arm-linux-musleabihf-cross.tgz
をダウンロードする。
pkg-config
pkg-configはマシンにインストールされているライブラリに応じて、コンパイラやリンカに渡すオプションを作ってくれる。
$ pkg-config --cflags --libs fontconfig
-I/usr/include/uuid -I/usr/include/freetype2 -I/usr/include/libpng16 -lfontconfig -lfreetype
そのときに必要なのが*.pc
ファイル。fontconfig.pc
がある場所を知るには、dpkg -S
を使うとよい。これに限らず、どのファイルがどのパッケージに属しているかを知るのに重宝する。
$ dpkg -S fontconfig.pc
libfontconfig-dev:armhf: /usr/lib/arm-linux-gnueabihf/pkgconfig/fontconfig.pc
libfontconfig-dev:amd64: /usr/lib/x86_64-linux-gnu/pkgconfig/fontconfig.pc
pkg-configをクロスコンパイルに使う、つまりARMのライブラリをpkg-configから参照できるようにするには、
PKG_CONFIG_ALLOW_CROSS=1
PKG_CONFIG_LIBDIR=/usr/lib/arm-linux-gnueabihf/pkgconfig
(パスは一例)
と設定する。PKG_CONFIG_LIBDIR
はPKG_CONFIG_PATH
より優先される。
こうしないと、pkg-config has not been configured to support cross-compilation.
とエラーが出る。
Cross Compiling With pkg-configは公式のガイド。
Raspberry Pi
バージョンによってアーキテクチャが微妙に異なるのが厄介。
- 幅広く対応することを優先するならば
arm-*
- パフォーマンスを優先するならば
armv7-*
など、アーキテクチャを絞るとそれ特有の最適化がかかるので嬉しい
マシンのtriple(arm-unknown-linux-gnu
など)がわからなければ、Buildrootでmake raspberrypi3_defconfig
として正解を盗むのが手っ取り早い(邪道だが)。オプションの一覧は make help
で閲覧できる。
ツールチェイン
要はGCCとldとar。特にライブラリを使わないのであれば、
- パッケージマネージャであるようなクロスコンパイル用のパッケージを入れる
- 自前でビルドする
という手段がある。cross
を使えばそのへんは勝手にやってくれる。
パッケージで入れると結局どこに入ったか探すのが面倒になるから、ソースコードを探してきて自分でやってもよい。ただしGCC, ld以外にもビルドしなければならない場合は面倒。Buildrootだとそのあたりはまとめてやってくれる。
Buildroot
バージョンの組み合わせさえ気にしなければ手っ取り早い(自分であるライブラリだけを新しくすることはできない)。sysrootはbuildroot/output/host/arm-*/sysroot
にある。基本的にmake xconfig
で設定して、make
すれば終わり。Kernelはいらないのでチェックを外す。
crosstools-ng
crosstools-ngはBuildrootと違ってカーネルやファイルシステムには手を付けず、ツールチェインだけをビルドする。 試したところ、なんかINRIAのレポジトリが落ちていてあるライブラリを使えなかった。それだけ手動で探してくれば大丈夫だろうか。
参考
- CentOS8 で rust の Raspberry Pi クロスコンパイルをやってみる 2020-07-07
- Cross-Compiling Rust apps for the Raspberry Pi 2020-10-22
- spotifydをRPi向けにクロスコンパイルする試行錯誤の記録
- Cross-compiling Rust programs for a Raspberry Pi from macOS 2020-04-28
- C compiler cannot create executables