feature image

2024年8月29日 | ブログ記事

クロスコンパイルRust

この記事は 夏のブログリレー 10日目です。1日遅れてしまいました...

夢のマルチプラットフォーム対応

私たちが開発したアプリを配布する場合、マルチプラットフォームに対応することが重要になります。開発者が使用しているマシンと同じOS/CPUアーキテクチャで動作するバイナリしか配布されていなければ、どれだけ便利なアプリでも使ってもらえません。かといって開発者が様々なOS/CPUアーキテクチャのマシンを揃えるのは大変です。そこで必要になるのが、あるOS/CPUアーキテクチャのマシンから他のOS/CPUアーキテクチャで動作するバイナリを作成するクロスコンパイルです。

この記事では、特に以下の条件を満たすRust製アプリをクロスコンパイルする場合に役立つあれこれを解説します。

キーワード

クロスコンパイルを行うにあたって押さえておきたい単語がいくつかあるのでまずはそれらを確認します。

target

プログラムを実行する環境のことです。Rustでは、targetは以下のように表記されます。

<arch><sub>-<vendor>-<sys>-<abi>

この表記をtarget tripleと呼び、それぞれの枠には以下の情報が入ります。

といってもあくまで緩い表記規則で、wasm32-wasiなど大きく異なるものもあります。rustcがサポートしているtarget tripleはrustc --print target-listで一覧することができます。target tripleの表記はおそらくLLVM/Clangに倣ったもので、そのLLVMによるとtripleと名前についているのは歴史的経緯だそうです。(llvm::Triple)

GoないしDockerではOSとアーキテクチャだけでtarget tripleに対応するものが定まります。特にDockerでは<os>/<arch>[/<variant>](<variant>はtarget tripleの<sub>にあたるもの)の形式で表記したものをplatformと呼ぶようです。platformの例としてlinux/amd64linux/arm/v6などがあります。

ref:

使用しているマシンのtarget tripleはrustc -vVの出力から確認できます。例えば、M1 Macbook Airのtarget tripleはaarch64-apple-darwin, 私の手元にあるHPラップトップのtarget tripleはx86_64-unknonwn-linux-gnuとなります。

host / target platform

ソースコードをビルドして実行可能ファイルを作成する環境(のtarget triple)と、その実行可能ファイルを使用する環境(のtarget triple)が異なるのがクロスコンパイルです。rustupのガイドでは、前者をhost platform、後者をtarget platformと呼びます。

ちなみにNixでは前者をbuild platform, 後者をhost platformと呼びます。
ref: Cross compilation — nix.dev documentation

例えばHPラップトップでビルドした成果物をUbuntu OSが載っているRaspberry Piで動かす場合は、各platformとtarget tripleの対応関係は以下のようになります。

platform target triple
host platform x86_64-unknown-linux-gnu
target platform aarch64-unknown-linux-gnu

dependency

プログラムには依存関係があります。

特に気を付けるべきは、Cライブラリの存在です。lddコマンドでそのプログラムが依存している共有ライブラリ(shared object)の情報がわかります。cargo initで生成されるごく単純なプログラムでも、lddの出力は以下のようになります。

linux-vdso.so.1 (0x0000...)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x0000...)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000...)
/lib64/ld-linux-x86-64.so.2 (0x0000...)

問題は、共有ライブラリはtarget platformにインストールされている必要があるということです(対応するヘッダファイルがコンパイル時に要求されるため、host platformでも用意されている必要があります)。

Goの場合はcgoが絡まない限りはlibcにすら依存しません。羨ましい

ただし、上記の例のように、依存している共有ライブラリがlibc(およびlibgcc)だけであれば問題はありません。先ほどのldd出力例はtarget tripleがx86_64-unknown-linux-gnuの状態でビルドした結果に対するものでしたが、cargo build --target x86_64-unknown-linux-muslのようにtarget tripleでmuslを指定するとlddの出力は以下のように変化します。

statically linked

このように、依存ライブラリが静的リンクを選択肢として提供している場合は、target platformで特別な用意を行う必要がなくなります。

ref: RustのLinux muslターゲット (その1:Linux向けのポータブルなバイナリを作る)

いざ実践

いい加減実際のアプリでのビルド例を見てみます。

サンプルのアプリ紹介

GitHub - H1rono/login-with-axum: Just login (with axum)
Just login (with axum). Contribute to H1rono/login-with-axum development by creating an account on GitHub.

Axumでログイン機能を提供するだけのアプリです。↓のコマンドで試すことができます。多分。

docker compose -f docker/compose.yml --env-file .env.dev up -d

Cargo.tomlには以下のように依存関係が記述されています。

sqlxnative-tlsに依存しており、特にLinuxではnative-tlsがopensslを通してlibsslに依存します。

この程度であればrustlsに切り替えてlibsslへの依存を回避できるのですが、今回はCライブラリへの依存例としてそのままにしてあります。

Dockerfile雛形

2024年版のDockerfileの考え方&書き方 | フューチャー技術ブログを参考に書いていきます。

builder stageがhost platform, debian-slim stageがtarget platformです。先ほども言及した通りlibsslへの依存があるため、debian-slimのステージでlibssl-devを入れています。(builder stageでは特に用意していませんが、rust:bookwormイメージでは最初から用意されているためです)

また、このDockerfileの通り、host platformでもtarget platformでもdebianベースのイメージを使用します。

揃えるもの

以降はtarget platformがx86_64-unknown-linux-gnuの場合を考えます。先ほど確認した通りlibsslへの依存があるため、host platformでは適切なrust toolchainに加えて以下のものを揃える必要があります。

Multiarch/HOWTO - Debian Wiki, ToolChain/Cross - Debian Wikiに記述がある通り、これらは以下のコマンドで揃えられます。

dpkg --add-architecture amd64
apt-get update
apt-get install crossbuild-essential-amd64 libssl-dev:amd64

これでインストールされたgccはx86_64-linux-gnu-gccとして提供され, ライブラリは/usr/lib/x86_64-linux-gnu, ヘッダーファイルは/usr/include/x86_64-linux-gnuに配置されます。また、これらの存在をrustの各クレートに通知する必要がありますが、以下の環境変数を指定することで可能です。

cargo config

上記の環境変数に加えて、target tripleに応じて使用するリンカを変える必要があります。これはCargoの設定をいじることで実現可能です。

Configuration - The Cargo Book

環境変数とリンカに加えて、ついでにbuild時のtarget tripleも設定したconfig.tomlは以下のようになります。

[build]
target = "x86_64-unknown-linux-gnu"

[env]
X86_64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR = "/usr/lib/x86_64-linux-gnu"
X86_64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR = "/usr/include/x86_64-linux-gnu"
TARGET_CC = "x86_64-linux-gnu-gcc"
TARGET_CXX = "x86_64-linux-gnu-g++"

[target.x86_64-unknown-linux-gnu]
linker = "x86_64-linux-gnu-gcc"

しれっとリンカにx86_64-linux-gnu-gccを指定していますが、x86_64-linux-gnu-ldだとビルドに失敗したのでこうしています。clangでクロスコンパイルしたい場合もリンカにgccを使うといいらしいんですが、ソースの記事を無くしてしまいました...

魔法のヘルパースクリプト

さて、ここまでをまとめると、クロスコンパイルの準備には以下のものが必要です。

なかなか大変ですね。そこで、「上記の準備工程全てをよしなに行ってくれるヘルパースクリプト」を生成するための 魔法 Pythonスクリプトを作成しました。

login-with-axum/docker/generate-helper.py at 3bc4e2c087d8f41534d5fab6b7a22ddf2e8999be · H1rono/login-with-axum
Just login (with axum). Contribute to H1rono/login-with-axum development by creating an account on GitHub.
魔法なので中身には触れない

こういう、「ちょっと複雑だけどあくまでヘルパーな作業」をやりたい時にPythonは便利ですね。このPythonスクリプトをDockerfileに組み込んで、クロスコンパイルが可能になりました。

実際にghcr.ioにマルチプラットフォーム対応のイメージをpushすることができました。

出来上がったものがこちら

multiplatform-image

その他やってないこととか

build script!?

Build Scripts - The Cargo Book

cargo build時にやりたい処理を走らせるのがbuild scriptです。このscriptには本当にやりたい処理をなんでも書けてしまって、なんとビルド時にインターネットから必要なファイルをダウンロードするといったこともできてしまいます。問題は、このbuild scriptがなんらかのCライブラリに依存している場合です。設定が甘いと、host platformで実行したいプログラムをtarget platform向けのコンパイラでビルドするような事態になりかねません。恐ろしや...

mips, i386対応

をやるつもりでPythonスクリプトを用意したんですが、色々とうまくいきませんでした。PRを出してくれたら泣いて喜びます。

cross-rs/cross

cross-rs/cross: “Zero setup” cross compilation and “cross testing” of Rust crates

実はここまで長ったらしく議論したあれこれは必要ないかもしれません。

pkg-config

色々調べてみたんですが、どうもbuild scriptとの兼ね合いが難しそうです。

windows, macOS向けのビルド

一体どうやるんでしょうね。

参考

おしまい

まる。

明日(今日)の担当は@masu_kouさんと@elさんと@masataroさんと@ch4tlaさん、そして@vPhosさんと@Cd_48さんです。お楽しみに!

H1rono_K icon
この記事を書いた人
H1rono_K

趣味プログラマー(大学生)

この記事をシェア

このエントリーをはてなブックマークに追加
共有
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記