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)。インストールしてから、cargocrossで置き換えると、あらかじめ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などツールチェインを自分で用意する。

  1. (初めてのコンパイルなら)rustupでアーキテクチャを追加する
  2. (project)/.cargo/configに追記する
  3. $ 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-tlsrustls-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>_LINKERCC, 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の出力するエラーメッセージは改行が崩れていなくてパッと目につくのだが、たまに的外れなことを書いてある。そのちょっと上にあるldgccが出力した崩れたエラーメッセージを読む。

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_LIBDIRPKG_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のレポジトリが落ちていてあるライブラリを使えなかった。それだけ手動で探してくれば大丈夫だろうか。

参考

Backlinks