はじめに
これはConoHa Advent Calendar 2024の12日目の記事です。
私は最近なんか手広くやっていますがたぶん本業は組み込みLinux開発ということになっており、息をするようにArm Linuxバイナリをひねり出す日々を送っております。そのためにいまのところは普通にローカルホストにクロス環境を構築しているわけですが、なんか昨今そういうのはDockerでやるのがナウでヤングじゃんという噂を小耳にはさみ、またCIの設定でわけわからんまま雰囲気でDockerイメージ作ったりしたのもそのままじゃよくないし、いいかげんおべんきょうしないとなあと思ったというわけです。
というわけで、この記事ではDockerを学んでRustのArm Linuxクロス環境をDockerコンテナー内に作り、それを使ってクロスビルドを行えるようになるぞ! という目標を設定し、今回はそのためのホスト環境をConoHa VPS上に構築します。これは気軽に立てられて用途が終わったら削除できる、またクラウド上にあれば出先からでもアクセスできるというメリットを享受しようというものです。
ヤングもすなるDockerといふものを検閲により削除
もしてみむとてするなり。
なお、この記事の執筆にあたり作成したファイルはGitHubに上げてあります。
サーバーを立てる
ではさっそくサーバーを立てていきましょう。VPS Ubuntu 24.04 RAM 1GBとかで適当に立てます。立ち上がったら、sudoができる一般ユーザーが公開鍵認証でSSHログインできるように設定していきます。あと、私は通勤電車からスマートフォンで操作したりするのでMoshも設定します。まあ、後者についてはほとんどの人は必要ないかもしれませんが...
ファイアウォールがデフォルトで有効になっており、SSH(TCP 22)とMosh(UDP 60000 - 61000)が通るように設定してあげる必要があります。してあげましょう。
そして今回のハマりポイントですが、ConoHa Ver.3.0になってセキュリティグループという概念が追加されています。VPSの外側にもファイアウォールのようなものがある感じですかね? こちらも同様に必要なポートを開けてあげましょう。
SSHなりMoshなりで接続できましたか? できたっぽかったら次に進みましょう。
Docker Engineをインストールする
続いてDocker Engineをインストールします。とはいってもやることは公式ドキュメントの内容そのまんまです。
まずはaptリポジトリを設定します。
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
設定できたらインストールします。
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
the Docker Communityが提供しているhello-worldイメージが実行できたら準備としてはOKみたいです。
$ sudo docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
動いたっぽい!
Lesson 1: Hello, World!を書く
準備ができたところで、では実際にDockerイメージの作り方を学んでいきましょう。ここではよいこのみんなのおやくそくとしてHello, World!をやってみます。hello-worldイメージの実行はさっきやったところですが、次はそれを自分で作れるようになるということが必要です。
以下のようなDockerfileを書きました。
# 元となるイメージ
FROM ubuntu:24.04
# イメージ作成時のコマンド
RUN apt update && apt install -y python3
# 作業ディレクトリ
WORKDIR /work
# ファイルのコピー
COPY hello.py /work
# コンテナ起動時のコマンド
CMD [ "python3", "/work/hello.py" ]
また、以下のようなPythonコードを書いて、hello.pyとして保存しました。
if __name__ == "__main__":
print("Hello, World!")
では、これらを使用してtestdocker:1というイメージをビルドしてみます。
$ sudo docker build -t testdocker:1 .
イメージのビルドがうまくいったようなら実行してみましょう。
$ sudo docker run testdocker:1
Hello, World!
動きました! まずは第一歩を踏み出しましたね。
Lesson 2: ファイル入出力を行う
目標を達成するためにはコンテナー内でコンパイラーを動かしたいわけですが、それにはホスト側からソースファイルを与えてバイナリファイルを得るという、ファイルの入出力ができないといけません。ひとまずそのへんの実験からしていきます。
以下のようなDockerfileを書きました。
FROM ubuntu:24.04
RUN apt update && apt install -y python3
WORKDIR /work
CMD [ "python3", "/work/hello.py" ]
また、以下のようなPythonコードを書いて、hello.pyとして保存しました。今回は標準出力の他にファイル greeting.txtにもHello, World!を出力してみます。
if __name__ == "__main__":
greeting = "Hello, World!"
print(greeting)
with open("greeting.txt", "w") as file:
file.write(greeting + "\n")
ではこれらを使用してtestdocker:2というイメージをビルドしてみます。
$ sudo docker build -t testdocker:2 .
イメージのビルドがうまくいったようなら実行してみましょう。この時、-vオプションでカレントディレクトリをコンテナーの/workにマウントするよう指定するのがポイントです。
$ sudo docker run -v .:/work testdocker:2
Hello, World!
ホスト側にあるhello.pyをコンテナー側のpython3で実行することができました。では、ファイルの出力はできているでしょうか。
$ cat greeting.txt
Hello, World!
ちゃんとできているようです。ファイルの入出力がともにできるということはわかったので、あとはPythonのインタプリターがRustのコンパイラーに変われば目標を達成できそうです。
Lesson 3: Rustのビルドを行う
ではいよいよ、Rustのビルドに挑戦してみましょう。いきなりクロスビルドだと話がややこしくなるので、まずはx86-64 Linuxのセルフビルドからやってみます。
実験台としては拙作のzatsuを使用します。とりあえずGitHubからソースコードを取ってきましょう。私は最近バージョン管理システムとしてJujutsuを使用しているので、Jujutsuリポジトリの初期化もおこないます。
$ git clone https://github.com/ygohko/zatsu.git
$ cd zatsu/
$ jj git init --colocate
余談です。2024年はバージョン管理システム Jujutsuを知り、導入したことが最大の作業効率改善になりました。私はGitのコマンドラインデザインが本当に苦手で、シンプルで合理的、柔軟で強力なJujutsuに大変助けられた1年でした。
まだ未実装の機能がある、バージョンアップで破壊的な仕様変更が入ることがある(最近も従来branchと呼ばれていたものがbookmarkと改称されたばかり)など未完成なところも散見されますが、Gitに不満がある方は一度試してみてはいかがでしょうか。興味がある方は以下に示すサイトをご参照ください。
さて、余談も終わってイメージの準備。以下のようなDockerfileを書きました。
FROM rust:latest
WORKDIR /work
CMD [ "cargo", "build" ]
the Rust Project developersが提供しているオフィシャルイメージを元に、起動したら/workでcargo buildが実行され、Rustプロジェクトのビルドが行われるようにします。では、testdocker:3というイメージをビルドしてみましょう。
$ sudo docker build -t testdocker:3 .
イメージのビルドがうまくいったようなら、zatsuのソースコードディレクトリに移動してから以下のように実行します。
$ sudo docker run -v .:/work testdocker:3
バイナリのビルドがうまくいったようなら確認してみましょう。
$ file target/debug/zatsu
target/debug/zatsu: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ab4633430c74eaf4088fed6e02d9719bfb9fcdda, for GNU/Linux 3.2.0, with debug_info, not stripped
$ target/debug/zatsu --help
Usage: zatsu [COMMAND]
Commands:
init Initialize a repository into this directory
commit Commit current files into this directory's repository
log Show logs of this directory's repository
get Get a file or directory that is specified
forget Remove stored revisions to shrink this directory's repository to specified size
upgrade Upgrade this repository
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
動いているようです。やったね!
Lesson 4: Armのクロスビルドを行う
ついに最終段階、Armのクロスビルドに挑みます。以下のようなDockerfileを書きました。
FROM rust:latest
# クロス環境のインストール
RUN apt update && apt install -y g++-arm-linux-gnueabihf libc6-dev-armhf-cross
RUN rustup target add armv7-unknown-linux-gnueabihf
RUN rustup toolchain install stable-armv7-unknown-linux-gnueabihf
WORKDIR /work
# リンカーの指定
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
CMD [ "cargo", "build", "--target", "armv7-unknown-linux-gnueabihf" ]
Lesson 3との違いはクロス環境のインストール、リンカーの指定、そしてコンテナ起動時のコマンドに--targetオプションが追加されることです。では例によって、testdocker:4というイメージをビルドしてみましょう。
$ sudo docker build -t testdocker:4 .
イメージのビルドがうまくいったようなら、zatsuのソースコードディレクトリに移動してから以下のように実行します。
$ sudo docker run -v .:/work testdocker:4
バイナリのビルドがうまくいったようなら確認してみましょう。
$ file target/armv7-unknown-linux-gnueabihf/debug/zatsu
target/armv7-unknown-linux-gnueabihf/debug/zatsu: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamical
ly linked, interpreter /lib/ld-linux-armhf.so.3, BuildID[sha1]=72c8a89e67d33c6f687c706ed6854e631adfa356, for GNU/Linux
3.2.0, with debug_info, not stripped
$ target/armv7-unknown-linux-gnueabihf/debug/zatsu
bash: target/armv7-unknown-linux-gnueabihf/debug/zatsu: バイナリファイルを実行できません: 実行形式エラー
ちゃんとArmのバイナリが出力されているっぽいことがわかりました。動作確認はできていませんが、まあたぶんしかるべき環境に持っていけば動くでしょうからこれで目標達成ということにしてしまいましょう...
おわりに
この記事ではConoHa上にサーバーを立てるところから始めて順を追ってDockerの動作を学び、実際にRustのクロスビルドが行えるようになりました。この方法を用いればローカルホストから隔離されたクロス環境を作ることができ、どんなにターゲットが増えてもホスト環境をクリーンに保つことができそうです。また、この方法はクロスビルドに限らず、古い処理系じゃないと動かないようなプロジェクトをお守りするなんて時にもたぶん有効でしょう(まあ、そんな機会はないほうがいいに決まっていますが...)。
そして、今回はConoHa VPS上に環境を作ったことで通勤中や外食の提供待ちなどの空き時間にも学習を進めることができ、大変助かりました。
というわけで、今回は自分用の記録としての側面が強い記事でしたが、お読みになったみなさんのお役に立つ機会がありましたら幸いです!