CIで実機(Rapsberry Pi 3 Model b+, HiFive Unleashed board)のdebian imageを自動作成する

動機

linux kernelの設定やboot partitionのカスタマイズ等で実機用のimage(sdcardに焼くimage)を柔軟に作成したい。できればCIでcommitごとに自動で完成品のimageが所定の場所(e.g) AWS S3など)にuploadされてほしい。

今回は、aarch64(Rapsberry Pi 3 Model b+)とriscv64(HiFive Unleashed board)でその機会があったので、いい感じにCIで自動化するのを試してみた。

割と試行錯誤的に行った&自分の知識の限度もあるのでもっと良いやり方があれば知りたいです。

CIでの作成(今回はgitlab CIで行った)なので、当然ながらCIのOSはx86_64, sdcardのimageはaarch64 or riscv64を想定しているので、クロスでバイナリが作成される必要がある。

また、sdcardにimageを焼く作業自体は(ロボット的な物理的自動化)なのでここでは述べない。sdcard readerを差し込んでddするだけ。

流れ

残念ながら完全にdockerの--privileged optionを用いずに一連の作業を自動化することができなかった(もしこれができれば完全にdocker containerの隔離された環境でsdcard image作成ができるので結構嬉しい)。やむなく、docker run の--privilegedを使用した箇所がある([*]と書いてある場所)。例えば、losetupは/dev/loopが関わっているし、debootstrapはchroot実行時に/proc/sys/fs/binfmt_miscが関わっており、それぞれdevfsとprocfsがmountされている前提条件にあるので。

ただ、可能な限り--privilegedを必要とせずdocker buildできる用に苦慮したので記事にしてまとめている。

今回は大まかに2つのstepを踏むことにした:

  1. 各partitionごとにdocker imageを用意しておく
  2. 1を利用してsdcard用のimageを作成する

各partitionごとのdocker imageの作成方法

Rapsberry Pi3の場合

sdcard挿入でRapsberry Pi3を立ち上げた際のrootfsは/dev/mmcblk0p2でboot loaderのバイナリは/dev/mmcblk0p1にあるようにする。この章ではそれぞれ、rootfs, bootloaderに対応するdocker imageをそれぞれ作成する。

rootfs

以下の手順で行う:

  1. debootstrap --arch=arm64 --foreign --include=... ${TARGET_DIR}で必要なpackageをdownloadする。
  2. ${TARGET_DIR}/usr/binにqemu-aarch64-staticを配置する
  3. [*]update-binfmts --enable qemu-aarch64chroot時にaarch64のバイナリを実行できるようにする
  4. [*]chroot ${TARGET_DIR} debootstrap/debootstrap --second-stagedebian rootfsが構成される
  5. [optional] chroot ${TARGET_DIR} /bin/bash -c '...'などでrootfsの設定
  6. mkfs.ext4 -d ${TARGET_DIR} -O 64bit ${ROOTFS} ${ROOTFS_SIZE}としてrootfs用(/dev/mmcblk0p2用)のimageを作成する。

1~3については、前の記事 を主に参照する。

3,4に関してchroot/proc/sys/fs/binfmt_miscを用いるため、docker imageではprocfsがmountされていない。mountはdocker runの--privileged optionがないと実行できない。

結局の所、3.から先はdocker buildで対応できない(--privilegedを付与して走らせる必要があるため)ので、docker run時にscriptを走らせることによって対応させるしかない。また、6でできたext4のimageはdocker containerの外に移す必要がある。

boot_loader

こちらは--privilegedを用いずに作成できた。(なので、docker buildのみで構築可能だ。)

Linux kernelは.config

CONFIG_CMDLINE="console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 noinitrd rw rootwait init=/lib/systemd/systemd"

にしてkernelをbuildして、Imageを作成しておく。

その他の部分のboot partion imageの作成自体は余り自分は詳しくないのだが、 https://github.com/OP-TEE/build/blob/3.10.0/rpi3.mk#L155-L171 のようなファイル群が用意されている必要がある。これらもdocker container内部で作成可能であることは確認済みである。

bootloaderのfilesystemのtypeはvfatなのだが、mkfs.ext4のように-d optionが用意されていないので、mcopyで直接vfatのimageに必要なboot時のfileを注入する必要がある。

mcopyを使わない場合だと、mount等で対応するのだろうがそれだと--privilegedを用いる必要性があり嬉しくない。

dd if=/dev/zero of=${BOOT_IMG} bs=32M count=1
mkfs.vfat -F16 -n BOOT ${BOOT_IMG}
find ${BOOT_DIR} -type f -exec basename {} \; | \
  xargs -I{} mcopy -v -i ${BOOT_IMG_PATH} boot/{} ::{}

HiFive Unleashed boardの場合

お次はriscv64の場合。 sdcard挿入でHiFive Unleashed boardを立ち上げた際のrootfsはpartition4で、boot loaderのバイナリはpartition2に、FSBLのバイナリはpartition3にあるようにする。partitionの設定含め次のsdcard用のimageを作成の章で説明することにする。

rootfs, BBL, FSBLに対応するdocker imageをそれぞれ作成する。

FSBL

こちらは--privilegedを用いずに作成できた。

FSBL(First Stage Boot Loader)は https://github.com/sifive/freedom-u540-c000-bootloader.gitmakeでbuildすればfsbl.binが生成されるので、これを使えば良い。

BBL

こちらも--privilegedを用いずに作成できる。

Hifive Unleashed boardの場合、buildrootのlinux kernelのCONFIGを以下のように変更する:

CONFIG_CMDLINE="earlyprintk noinitrd rootfstype=ext4 root=PARTUUID=${partition_guid_1} rw rootwait init=/lib/systemd/systemd"

root=PARTUUID=...の表記については、https://qiita.com/pluser/items/645531b570dfcc65e324 が詳しい。

partition_guid_1はsgdiskの--partition-guid=1:${partition_guid_1}で対応させておく。sdcard用のimageを作成のときにおって説明するが、uuid形式ならば任意。

今回はriscv-pkを用いたがOpen SBIでも基本的には同じ様にできると思う。

rootfs

こちらはchroot debootstrap --secondstageのあたりで--privilegedの助けを借りる必要があった。

Rapsberry Pi3と手順はほぼ同じ。 前の記事 にも似たような記事を書いたので省略。

--arch=riscv64として、qemu-riscv64-staticを用意すれば問題ないと思う。

sdcard用のimageを作成

[*]と書いてある場所はdocker runの--privileged optionをつける必要がある。

  • 各partitionごとにdocker imageを用意しておく。これらは手順の変更点があれば、docker buildされて常に新しい状態に更新される
    • armならば、bootloaderのpartitionとrootfsのpartition用にそれぞれdocker imageを用意する
    • riscvならば、FSBLのpartitionとBBLのpartitionとrootfsのpartition用にそれぞれdocker imageを用意する
  • ddでimageの本体を作成し、sgdisk(HiFive Unleashed boardの場合) or sfdisk(Raspberry pi3の場合)でpartitionを作成する。
  • [*]losetupでimageの本体をmountする。
  • 上のdocker imageの内部のpartition imageを本体のimageのpartition上にコピーする(ddなどを用いて)
  • sdcardに焼くimageが完成したので、これをAWSのS3などにuploadしておく

mountあたりまでは、 https://stackoverflow.com/questions/40356259/mount-linux-image-in-docker-container/40360670#40360670 と似たような感じ。

sgdisk/sfdiskを用いたpartitionの作成

Rapsberry Pi3の場合

GPTでなく、MBRである必要があるみたい^1で、sgdisk(GPT用のpartition formatコマンド)ではなくsfdisk(もしくはparted)を用いる。sfdiskを用いると、

cat | sfdisk sdimage.bin <<-EOF
label: dos
label-id: 0x00000000
device: sdimage.bin
unit: sectors

sdimage.bin1 : start=        2048, size=       65536, type=e, bootable
sdimage.bin2 : start=       67584, size=   12000000, type=83
EOF

のようにディスク情報をinputとして流し込めるので便利。https://sweetcafe.jp/?*20190314-224648 にもある通り、バックアップの意味も兼ねて既にディスクがあるのなら引数sfdisk -dでディスク情報を抜き出すことができるので、ミスしなくて済む。

後は、bootのmountの設定として

cat >${TARGET_DIR}/etc/fstab <<_END
/dev/mmcblk0p2 / ext4 defaults,rw,relatime 1 1
tmpfs /tmp tmpfs rw,nosuid,nodev 0 0
_END

をRapsberry pi3のrootfsのdocker imageの設定として書いておく。

HiFive Unleashed boardの場合

こちらはGPT形式でpartitionを作成する。(というより、Rapsberry Pi3はGPT非対応っぽいのでやむなくMBR形式でpartitionを作成した)

sgdisk --zap-all --clear \
  --new=1:2048:262143 \
  --new=2:262144:262399 \
  --new=3:264192:329727 \
  --new=4:524288:12580863 \
  --change-name=1:'U-boot boot partition' \
  --change-name=2:'HiFive FSBL' \
  --change-name=3:'HiFive BBL' \
  --change-name=4:'Root partition' \
  --typecode=1:EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 \
  --typecode=2:5B193300-FC78-40CD-8002-E86C45580B47 \
  --typecode=3:2E54B353-1271-4842-806F-E436D6AF6985 \
  --typecode=4:0FC63DAF-8483-4772-8E79-3D69D8477DE4 \
  --partition-guid=1:${partition_guid_1} \
  --partition-guid=4:${partition_guid_4} \
  $(IMAGE_BIN)

partition_guidは何でも良いがCONF CONFIG_CMDLINEのroot=PARTUUID=${partition_guid_1}と合わせること。

対してtypecodeは https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs のtableと対応している。

後は、rootfsのdocker image作成時に

cat >${TARGET_DIR}/etc/fstab <<_END
PARTUUID=${partition_guid_4} /     ext4 defaults,rw,relatime 1 1
tmpfs /tmp tmpfs rw,nosuid,nodev 0 0
_END

を加える。

その他の感想

  • debootstrapはdebian環境を作成するコマンドとして非常に便利だが、あまり安定ではない。CIで実行すると時々ネットワークエラなどで失敗するようだ。自分の場合は https://gist.github.com/knknkn1162/8f82806800df14f53e669e28477f9568 のようなbsdmainutilsのエラーが出たりして困ってしまった。ただ、時間が経てば治っていたので真の原因はよくわからない。

  • 今回は実機でのimageを想定したが、qemuでも似たようなことをやれば動くと思う。qemuだとbuildrootで容易にqemu用のimageを作成できるが、私が携わっているプロジェクトの場合、buildrootだとapt-getができないなどの関係で、更にリッチなディストリビューション(=debian)がほしいという要求があった。

  • 特にrootfsは基本的に差し替え不要でBBLなどをアレンジすれば問題なさそう(というかアレンジが容易にできるということを考慮してpartitionごとにdocker imageとして環境を用意した)。現にriscv64のqemuの場合だと、BBLを違うものに変更しさえすればqemu上でもdebian環境が動作した。

  • 今回はsdcard用のimageを再生成するのを自動化する試みだったが、linux kernel上でなんかmoduleとかを作る場合は再起動とかで済むかな、と思うのでこの記事の需要のほどは全くわかっていない。また、それよりレイヤーの低めの組み込みOS界隈だとdocker imageで部品を管理して組み合わせるという方向よりはyoctoで一箇所にまとめるプロジェクトが主流そう。yoctoは一箇所に固める関係上、dockerとものすごく相性が悪いので、今回のようなCIによる自動化にニーズが有るのかさえよくわかっていない。

  • dockerはaarch64用のimageを作ることはできるみたいだが、riscv64用は公式にはサポートしていなさそうなので、update-binfmts --import /tmp/riscv64を介在させてdebootstrapを起動させる必要がありそう。逆にこれがサポートされていれば少なくともdebootstrapは--second-stageにて--privilegedを付与しなくて済む?? aarch64の場合はすでに可能かどうかはまだ試していない。