動機
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を踏むことにした:
- 各partitionごとにdocker imageを用意しておく
- 1を利用してsdcard用のimageを作成する
各partitionごとのdocker imageの作成方法
Rapsberry Pi3の場合
sdcard挿入でRapsberry Pi3を立ち上げた際のrootfsは/dev/mmcblk0p2でboot loaderのバイナリは/dev/mmcblk0p1にあるようにする。この章ではそれぞれ、rootfs, bootloaderに対応するdocker imageをそれぞれ作成する。
rootfs
以下の手順で行う:
debootstrap --arch=arm64 --foreign --include=... ${TARGET_DIR}
で必要なpackageをdownloadする。${TARGET_DIR}
/usr/binにqemu-aarch64-static
を配置する- [*]
update-binfmts --enable qemu-aarch64
でchroot時にaarch64のバイナリを実行できるようにする - [*]
chroot ${TARGET_DIR} debootstrap/debootstrap --second-stage
でdebian rootfsが構成される - [optional]
chroot ${TARGET_DIR} /bin/bash -c '...'
などでrootfsの設定 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.gitをmake
で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の場合はすでに可能かどうかはまだ試していない。