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

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

https://github.com/outlandkarasu-sandbox/ldc-rpi-crosscompile

## 前提条件

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

## 手順概要

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

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

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

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

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

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

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

https://github.com/raspberrypi/tools

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

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

## LDCをインストールする。

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

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

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

* https://dlang.org/install.html
* [kotetさんによる「#dlang インストールスクリプトの使い方」](https://blog.kotet.jp/2017/12/d-install-script/)
* [D言語環境構築 2019年版](https://qiita.com/outlandkarasu@github/items/faa555d5c1d1d19a8fa4) ※手前味噌

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

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

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

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

```sh
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
```

**エラーが出ました。**

なんか、`curl`の`SSL routines`における`tls_process_ske_dhe`で`dh key`が`too 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の設定を書き換えます。**(他にもっと良い方法があるかもしれません……)

```sh
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](https://www.7-zip.org/)が、実は`img`ファイルの展開に対応しているとのことです。

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

```sh
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`](https://wiki.dlang.org/Building_LDC_runtime_libraries)というツールがあり、`cmake`・`ninja`などを駆使したランタイムライブラリのビルドを行えます。

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

```sh
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を使った簡単な画像表示プログラムです。

https://github.com/outlandkarasu-sandbox/ldc-rpi-crosscompile/tree/master/dmanshow

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

```
sudo apt install libsdl2-dev libsdl2-image-2.0
```

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

![IMG_0903.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3735/f0056878-bfda-af4c-9186-9b9b8eb36182.jpeg)

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