📝

Docker開発環境(4): docker-buildenvによるYoctoビルド環境

2021/08/08に公開

前回の続きです。

https://zenn.dev/anyakichi/articles/3b5b379975be74

まずは docker-buildenv について手短に説明します。

docker-buildenv には第2回、第3回で説明した entrypoint.sh と buildenv.sh が含まれています。サンプルのものはかなり簡略化したものなので、もうちょっと全体的にちゃんと作ってあるのですが、やっていることそのものはあまり変わりません。また、これらに加えて din.sh というスクリプトも含まれています。

  • entrypoint.sh
    • サンプルでは setpriv を使ったが、gosu, setpriv, sudo を順番にフォールバックして使うようになっている。
    • setpriv が使える場合にはこれが良いが、環境によって入っていなかったりバージョンが古かったりというケースがあるので、そのようなときにシングルバイナリで入れられる gosu で差し替えられるようになっている。
    • sudo は sudo そのものがプロセスとして残ってしまうのであまりおすすめではない。
  • buildenv.sh
    • 実行前にこれから実行する予定のコマンドを表示する confirm 機能などがついている。confirm を省略する -y オプションや、実行されるコマンドを表示だけする -p オプション、ビルドレシピそのものを表示する -d オプションなどが含まれる。
    • 前回 extract, setup, build の alias を設定したが、設定すべき alias を出力してくれる buildenv init が用意されている。
    • バックスラッシュでコマンド行が複数行にまたがる場合でも適切にパース可能。
    • ドロップインで extract などの手順を複数のファイルに分けて格納することができる。
    • 直接実行モード(後述)をサポートしている。
  • din.sh
    • Docker 環境に入る際に、毎回
      $ docker run --rm -it -v $PWD:/build -w /build builder
      
      という引数を書くのが面倒なので、単に
      $ din builder
      
      と起動できるようにするラッパースクリプト。
    • 追加オプションはそのまま docker run に渡されるので、
      $ din -p 8000:8000 builder
      
      のような起動が可能。

Yocto のビルド環境を作成する

実はもう既にあるのですが、これをベースに構成方法のコツなどを解説したいと思います。

https://github.com/anyakichi/docker-yocto-builder

ひとまず使ってみる

完成イメージをつかむために、既存の Docker 環境を実際に使ってみましょう。簡単のために din スクリプトはインストールしておきます(PATH も通しておいてください)。

$ curl -o ~/.local/bin/din \
    https://raw.githubusercontent.com/anyakichi/docker-buildenv/main/din.sh

作業用ディレクトリを作ってコンテナの中に入ります。今回は Yocto の dunfell ブランチからビルドをしてみます。

$ mkdir yocto-1 && cd $_
$ din anyakichi/yocto-builder:dunfell

あとは extract, setup, build でビルドするだけですね(手順自体は簡単ですが、ビルドは結構時間がかかります)。

builder@yocto-1:/build$ extract
Extract commands:

  $ git clone -b dunfell http://git.yoctoproject.org/git/poky.git

Continue? (buildenv extract -h for details) [Y/n] y
==> git clone -b dunfell http://git.yoctoproject.org/git/poky.git
Cloning into 'poky'...
remote: Enumerating objects: 523530, done.
remote: Counting objects: 100% (523530/523530), done.
remote: Compressing objects: 100% (123344/123344), done.
remote: Total 523530 (delta 392139), reused 523374 (delta 392017)
Receiving objects: 100% (523530/523530), 176.30 MiB | 5.02 MiB/s, done.
Resolving deltas: 100% (392139/392139), done.
builder@yocto-1:/build$ setup
(略)
builder@yocto-1:/build/build$ build
Build commands:

  $ . <(buildenv setup)
  $ bitbake core-image-minimal

Continue? (buildenv build -h for details) [Y/n]
==> . <(buildenv setup)
==> bitbake core-image-minimal
(略)

core-image-minimal ではなく core-image-sato が作りたいという場合には、コンテナ内から build ではなく直接 bitbake を叩けば OK です。

builder@yocto-1:/build/build$ bitbake core-image-sato

このまま開発しちゃいたい人は、コンテナ内から yocto-layer create して、ホスト環境でファイルをいじってコンテナから bitbake core-image-sato とかしてもらえれば OK です。

Yocto なんて触ったことねーよ、という人でも、5 分かそこらでビルドを始めるところまで到達してしまいます。プロジェクトに新メンバーが入ってとりあえずビルドしてもらう、もしくは手元の環境が手狭になったので別の環境でビルドをしたい、という場合でも、OS のインストールなど本当に基本的な部分を除けばビルド開始に到達するまでの時間は 5 分くらいです。これが Docker をビルド環境として採用することによる最大のメリットといってもいいでしょう。

イメージのバリエーション

今回は dunfell を使いましたが、krogoth から kirkstone までのバージョン向けのイメージが既に作成済みです(kirkstone は未リリースですが)。dunfell のところを krogoth などに変えてください。

$ din anyakichi/yocto-builder:krogoth

また、ベースのイメージは xenial, bionic, focal がそれぞれ用意してあります。上記の krogoth だと xenial ベースが起動するのですが、focal で krogoth が良い場合には、

$ din anyakichi/yocto-builder:focal-krogoth

とできます。

単に dunfell や krogoth とした場合は、その Yocto のバージョンがリリースされた時点で最新の Ubuntu の LTS で起動します(基本的にはこれが Yocto として推奨している組み合わせです)。focal-krogoth などは作成時に単にループで生成できるのでしてありますが、これでビルド可能かどうかを個別に確認しているわけではありません。

Docker 環境を作成する

それでは上記のような環境を作成してみましょう。まずは Yocto とは関係なく、docker-buildenv を使う場合のベストプラクティスは以下のようになります。

  • docker-buildenv は git submodule か git subtree で リポジトリに取り込む。
    • かつてはイメージを CI でビルドするときに submodule だと checkout が面倒なことがあったのだが、今は submodule で良いと思う。
  • /etc/buildenv.d 以下に配置するレシピファイルは、extract.40.md などの形式で markdown ファイルで配置する。
    • 実際には拡張子は何でもよく、buildenv 自体は最初のドットの前までをコマンドだと解釈する。
    • 数字を入れておくとドロップイン的に使うことができる。ビルド環境を拡張する際に extract.40.md を残したまま extract.60.md を追加して追加のソースコードをダウンロードするようなことが可能。
    • 数字は前後に追加の余地を残すために 40 くらいが良いと思われる(実際にはなんでもよいし、ドロップインが必要なければ extract.md というファイルを置くこともできる)。
    • setup は setup 済みの状態から実行されても無害なようにする(何度実行しても大丈夫なように)。
    • build の前には setup をする(理由は後述)。
  • イメージには sudo を入れておき、builder からパスワード無しで sudo できるようになっていると何かと便利。これをやっておかないと「今だけちょっとパッケージを入れて試してみたいことがある」という場合に面倒(docker exec で対応は可能)。

次に Yocto のビルド環境を作ることを考えていきます。ビルド手順については前回も記載したとおり以下のような流れでした。

https://docs.yoctoproject.org/current/brief-yoctoprojectqs/index.html

  • サポートしているビルドホストを用意しろ。
  • apt-get でパッケージを入れろ。
  • poky を git clone しろ。
  • oe-init-build-env でビルド環境をセットアップしろ。
  • bitbake コマンドでビルドしろ。

まず、ビルド環境のベース(ディストリビューション)について考えます。Yocto としては Supported Linux Distributions として何種類かのディストリビューションを公式にサポートするものとして定義しています。最近は記載がなくなったのですが、以前は Yocto の特定のバージョンがリリースされた時点での Ubuntu の最新 LTS が特に recommended とされていました。ということでベースは ubuntu の Docker イメージを使うことにします。

次に apt-get で必要なパッケージ類を入れていきます。基本的にはマニュアルに書いてあるものをコピペで入れていきます。古いバージョンの Yocto もビルドできるように、以前のマニュアルに書いてあったものも含めてインストールしておきます。このあたりは実際の Dockerfile を見ていただいたほうがはやいかもしれません。

https://github.com/anyakichi/docker-yocto-builder/blob/main/Dockerfile

gosu, language-pack-en, sudo, tmux を追加で入れていますが、本当は Ubuntu 20.04 なら setpriv があるので gosu はいらないのですが手順の共通化のため、sudo は上記のベストプラクティスに沿って入れています。language-pack-en とそのあとの update-locale はロケールの設定ですが、Docker の ubuntu イメージはインストーラーから入れるものからはいろいろ省かれているものがあるので、思わぬ基本パッケージを手順書からは追加で入れないといけない場合もあります。tmux は bitbake でカーネルの menuconfig する際に必要だから入れていますが、これはどちらかといえば公式ドキュメントの記載漏れのような気はします。

あとはプロキシ環境下で git プロトコルで git clone するための oe-git-proxy をインストールするなど、Yocto に詳しい人なら知ってるかもしれないくらいの細かいこともいろいろやってますが、依存パッケージの導入としてはそのくらいでしょうか。

次に extract については poky を git clone すれば良いのでだいたいこうなります。

extract.40.md
Clone poky repository.

    $ git clone -b ${YOCTO_BRANCH:-master} \
        http://git.yoctoproject.org/git/poky.git

setup は oe-init-build-env という Yocto の専用のスクリプトがあるのでそれを使います。ただし、setup を setup 済みの状態で呼び出しても安全なように、既に setup 済みであれば再 setup はしないなどの対策は入れておきます。

setup.40.md
Skip setup if setup is already done.

    $ [[ "\${BBPATH}" ]] && return 0

Setup yocto build environment.

    $ source poky/oe-init-build-env build || return 1

Create auto.conf for our build environment.

    $ rm -rf conf/auto.conf
    $ [[ "${YOCTO_MACHINE}" ]] && echo "MACHINE = \"${YOCTO_MACHINE}\"" >> conf/auto.conf
    $ [[ "${YOCTO_DL_DIR}" ]] && echo 'DL_DIR = "${YOCTO_DL_DIR}"' >> conf/auto.conf
    $ [[ "${YOCTO_SSTATE_DIR}" ]] && echo 'SSTATE_DIR = "${YOCTO_SSTATE_DIR}"' >> conf/auto.conf

docker-buildenv の buildenv スクリプトでは環境変数が使えるのですが、これが解釈されるタイミングが 2 通りあります。

  • buildenv スクリプト自体が実行されるとき
  • buildenv スクリプトが生成したあとのコマンドが実行されるとき

コマンド実行時に解釈されてほしい(buildenv に解釈されてほしくない)場合は $ の前にバックスラッシュを入れています。

実際どちらで解釈させても実行時には大差ないことは多いのですが、もともと buildenv には「動く手順書」という思想があり、buildenv -d で手順書を表示させたときにどちらが自然かという発想で使い分けがされます。上記の ${BBPATH} を確認している箇所はエスケープされているので、これを表示させると

builder@yocto-1:/build/build$ buildenv setup -d
Skip setup if setup is already done.

    $ [[ "${BBPATH}" ]] && return 0

のように出力されます。これは「BBPATH に何か値が入っていれば oe-init-build-env が実行済みなはずなので return する」ということをマニュアルとして示しています。実際 BBPATH には /build/build という値が入るので、もしもエスケープしなければ

builder@yocto-1:/build/build$ buildenv setup -d
Skip setup if setup is already done.

    $ [[ "/build/build" ]] && return 0

と表示されてしまいますが、これだと return 0 されるのはわかりますがそれがどういう意味なのかわかりません。一方エスケープされていない extract では

builder@yocto-1:/build/build$ buildenv extract -d
Clone poky repository.

    $ git clone -b dunfell \
        http://git.yoctoproject.org/git/poky.git

のようにブランチ名が実際に指定される値で置き換えられて出力されます。dunfell の代わりに $YOCTO_BRANCH と表示されても、やはりマニュアルとしては「その値はいったい何なの?」という説明を書かなくてはいけません。

だいぶ話がそれてしまいましたが、最後に build の定義です。

build.40.md
Setup build environment before building.

    $ . <(buildenv setup)

Build your firmware by bitbake.

    $ bitbake ${YOCTO_BITBAKE_TARGET}

事前に setup を行ったあとで、bitbake を実行します。なお、手順書ファイル内の $YOCTO_BRANCH$YOCTO_BITBAKE_TARGET は Dockerfile 内で指定されているもので、ちょっとした派生イメージは環境変数を変えるだけで作れるようになっています。

なお、setup が再実行可能なこと、build で setup をすることは buildenv の直接実行モードとの兼ね合いで推奨されます。

以上、基本的には Dockerfile と extract, setup, build の markdown ファイルを作成すればビルド環境の作成はほぼ完了です。実際にビルド環境を作るには対象とするビルドシステムに対する深い理解が必要なことも多いのですが、記述しなければいけない分量自体はあまり多くならないことが多いです(例えば setup で auto.conf を使用するあたりはそれなりにわかっている人でないと発想が出てこないと思います)。

直接実行モード

最後に直接実行モードに触れて終わりにしようと思います。これまでの例では「コンテナの中に入って、コマンドを叩く」というやり方をしてきました。しかし実はコンテナの中に入る必要は必ずしもありません。

たとえばソースコードの取得だけであれば

$ mkdir yocto-1 && cd $_
$ din anyakichi/yocto-builder:dunfell extract

のように din の最後に extract を書けば実行できます。これを直接実行モードと呼んでいます。ビルドをする場合には同様に

$ din anyakichi/yocto-builder:dunfell build -y

のように書くことができます。このとき setup をするタイミングがないので、build には setup を含めるのが良い、ということなのでした。

buildenv に存在するコマンドであれば上記のように直接優先的に実行されます。存在しなければ普通のコマンドとして実行されるので、

$ din anyakichi/yocto-builder:dunfell ls
$ din anyakichi/yocto-builder:dunfell bash -c '. <(buildenv setup); bitbake core-image-sato'

のような書き方もできます。場合によってはこの呼び出し方法の方が便利な場合もあるので、覚えておくと良いです。

次回はキャッシュを利用したビルドの高速化方法についてと、今回の Yocto ビルド環境をベースに派生したビルド環境の構築方法について見ていきます。

Discussion