D言語(LDC)でRaspberry Pi向けのバイナリをクロスコンパイルする


D言語のLLVMフロントエンドであるLDCでRaspberry Pi(Arm)向けのバイナリをクロスコンパイルしてみました。
それなりに大変だったので、ここに手順を残しておこうと思います。
なお、環境構築までの手順を自動化したDockerファイルもこちらに置いてあります。

前提条件

  • Debian/GNU Linuxのdockerイメージのdebian:bullseye-slimから構築しています。
  • クロスコンパイルの実行マシンはx86_64です。
  • D言語のコンパイラにはLDCを、ビルドツールにはdubを使用します。
  • RPi側にはRaspbian Busterをインストールし、その上で自分のバイナリを動かします。

手順概要

  1. 必要ツールをインストールする。
  2. RPiクロスコンパイル用のツールをダウンロードする。
  3. LDCをインストールする。
  4. RPiのシステムイメージをダウンロードする。
  5. RPiのシステムイメージの中からライブラリ等のファイルを抽出する。
  6. LDCのD言語ランタイムライブラリを、ツールを使ってビルドする。
  7. 自分のプロジェクト用に設定ファイル(ldc2.conf)を書く。
  8. 自分のプロジェクトをdubでビルドする。

必要ツールのインストール

クロスコンパイルのために、まず普通のLinuxでのビルド環境を整えます。

sudo apt update
sudo apt install curl build-essential git libxml2 ninja-build cmake

libxml2ninja-buildcmakeはLDCのランタイムライブラリのビルドに必要です。gitは、RPiクロスコンパイル用ツールのダウンロードに必要になります。

RPiクロスコンパイル用のツールをダウンロードする。

RPiのArm向けツールチェインは、以下のGithubリポジトリにバイナリ等が丸ごと公開されています。
こちらをgit cloneして使用します。

git clone https://github.com/raspberrypi/tools

root権限等は不要です。作業用の通常ユーザーで問題ありません。

LDCをインストールする。

D言語のインストールスクリプトでLDCをインストールします。
こちらも作業用の通常ユーザーで問題ありません。

curl -fsS https://dlang.org/install.sh | bash -s ldc

インストールスクリプトについては、詳しくは以下が参考になります。

RPiのシステムイメージをダウンロードする。

さて、ビルト用のツールはGithubのtoolsリポジトリから取得しましたが、こちらにはRPiで動いているRaspbianのライブラリは付属していません。
しかし、実用的なプログラムをビルドしようと思うと、どうしてもRaspbian付属のライブラリ等が必要になります。
そこで、公開されているイメージファイルをダウンロードし、/lib/usr/libなどを抽出することにしました。

(今回はビルド環境構築をDockerで自動化するために頑張りましたが、RPiが近くにある状態で手元で簡単に試すときは、単純にRPiからscp等で持って来ればよいと思います)

というわけで、先ほどインストールしたcurlでさっそくダウンロードです。

curl -L https://downloads.raspberrypi.org/raspbian_lite_latest -o /tmp/raspbian.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (35) error:141A318A:SSL routines:tls_process_ske_dhe:dh key too small

エラーが出ました。

なんか、curlSSL routinesにおけるtls_process_ske_dhedh keytoo smallとか言っています。

ぐぐったところ、これはどうもDebian/GNU LinuxのBusterからOpenSSLのバージョンが上がり、DH鍵交換アルゴリズムの鍵長が脆弱性対策で長くなったため、鍵が短いホストだとエラーが出るようです。

つまり、RPiのダウンロードサイトの問題です。

参考URL
* https://tech.bank.co.jp/entry/2019/07/29/170000
* https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=907788

上記のissueが言っているように、仕方なくダウンロード中だけOpenSSLの設定を書き換えます。(他にもっと良い方法があるかもしれません……)

sudo cp /etc/ssl/openssl.cnf /etc/ssl/openssl.cnf.bak
sudo sed -i 's/CipherString = DEFAULT@SECLEVEL=2/#CipherString = DEFAULT@SECLEVEL=2/g' /etc/ssl/openssl.cnf
curl -L https://downloads.raspberrypi.org/raspbian_lite_latest -o /tmp/raspbian.zip
sudo mv /etc/ssl/openssl.cnf.bak /etc/ssl/openssl.cnf

書き戻しを忘れないように注意です。

RPiのシステムイメージの中からライブラリ等のファイルを抽出する。

さて次に、ダウンロードしたzipファイルの中のimgファイルから/libなどを抽出します。

zipを解凍するとimgファイルがあるので、それをループバックデバイスとしてマウントすれば良いのですが、今回使っているDockerではループバックデバイスが普通には使えません。
それに、単にファイルの中身を抽出するだけなのにroot権限が必要になるのも微妙なので、他の方法を調べました。

7-Zipを使用してimgファイルからファイルを取り出す

Windowsでよくお世話になっている7-Zipが、実はimgファイルの展開に対応しているとのことです。

こちらであれば、Dockerなどループバックデバイスが使用できない環境でも、7-Zipをインストールできればimgファイルの展開が可能です。
Raspbianのイメージファイルを早速展開してみます。以下のコマンドで、./sysroot内にRaspbianの/lib/usr/include/usr/lib/opt/vcが展開されます。

sudo apt-get install p7zip-full
7z e -so /tmp/raspbian.zip '*.img' > raspbian.img
7z e -spf -o/tmp /tmp/raspbian.img 1.img
7z e -spf -o./sysroot /tmp/1.img lib/ usr/include/ usr/lib/ opt/vc/

LDCのD言語ランタイムライブラリを、ツールを使ってビルドする。

ようやくD言語を使うところまで来ました……。といってもまだ下準備です。

LDCを単純にインストールしただけでは、インストール先のシステムのためのランタイムライブラリしか持っていません。
別のシステム向けにビルドを行うためには、別のシステム向けのランタイムライブラリが必要です。

幸い、LDCにはldc-build-runtimeというツールがあり、cmakeninjaなどを駆使したランタイムライブラリのビルドを行えます。

ビルドにはLDCを使用するので、先ほどinstall.shでインストールしたスクリプトを使ってLDCをアクティブにします。

source ~/dlang/ldc-*/activate
CC=~/tools/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc ldc-build-runtime --ninja --dFlags="-w;-mtriple=arm-linux-gnueabihf"

ldc-build-runtime実行時のCC=には、使用するCコンパイラ(今回はRPiツールチェインのarm-linux-gnueabihf-gcc)を指定します。

なお、私の環境では上記ビルドが最初の方で(0/98とか出ているところで)失敗することがありましたが、中間ファイルが残っている状態で再度実行するとうまくいきました。

Dockerビルド中にも失敗するので、ここだけ手動実行するようスクリプト(build_runtime.sh)だけ用意しています。

自分のプロジェクト用に設定ファイル(ldc2.conf)を書く。

ランタイムライブラリまでビルドできたら、いよいよ自分のプロジェクトをRPi向けにビルドします。

まず、プロジェクトのルートディレクトリにLDCの設定ファイルを配置します。

ldc2.conf
"arm-.*linux-gnueabihf":
{
    switches = [
        "--mtriple=arm-linux-gnueabihf",
        "--mcpu=arm1176jzf-s",
        "-defaultlib=phobos2-ldc,druntime-ldc",
        "-link-defaultlib-shared=false",
        "-gcc=/home/ldc/tools/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc",
        "--linker=bfd",
    ];
    post-switches = [
        "-I%%ldcbinarypath%%/../import",
    ];
    lib-dirs = [
        "/home/ldc/ldc-build-runtime.tmp/lib",
        "/home/ldc/sysroot/lib/arm-linux-gnueabihf",
    ];
};

こちらを配置した状態でdub build --arch=arm-linux-gnueabihfを実行すると、RPi向けのビルドが行われるようになります。

ldc2.confで設定しているのは以下のようなことになります。

  • --mtriple ビルドターゲット等指定。
  • --mcpu RPiのCPU(RPi1・ZERO)指定。
    • このCPU向けの命令であればRPi2・3などでも実行可能です。
  • -gcc リンク時に使用するGCCのパスを指定する。今回はもちろんRPi用ツールチェインのもの。
  • --linker リンカー指定。
    • デフォルトではgoldが使用されますが、RPiツールチェインにはgoldはないため、bfdを指定し直しています。
  • その他インポート・ライブラリパスなど指定

なお、--linkerスイッチの指定がないと、リンク時に以下のようなエラーが出ます。

Linking...
collect2: fatal error: cannot find 'ld'
compilation terminated.
Error: /home/ldc/tools/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc failed with status: 1

自分のプロジェクトをdubでビルドする。

上記まで完了すれば、自分のdubプロジェクトでdub build --arch=arm-linux-gnueabihfすることで、RPi向けのバイナリがビルドできます。

今回はdmanshowというサンプルプログラムを用意しました。これはSDL2・SDL_imageを使った簡単な画像表示プログラムです。

RPi側で実行するためには、SDLのインストールが必要です。

sudo apt install libsdl2-dev libsdl2-image-2.0

これで、scpなどでdmanshow実行ファイルをRPiに転送すると、こちらの画像が表示されます。

使う機会のなかったRPi3と公式LCDが見事に有効活用されました!