🔨

BuildrootとGitHub Actionsでつくる、自分だけのRaspberry Pi

2023/12/07に公開

この記事は 長野高専 Advent Calendar 2023 7日目のものです。


初めての方ははじめまして。そうでない方はこんにちは。Enchanです。
卒論前にカレンダーの枠を設定したせいでスケジュールが滅茶苦茶になってしまいました
自業自得です。

長野高専に限らず、研究やホビーでRaspberry Piを使っている方はたくさんいらっしゃることと思います。そんな中で慢性的に悩まされていたとある不満が 卒研で遂にピークに達したため、根本的に解決することにしました。

はじめに

Raspberry Pi。

Raspberry Pi 3 model B+
Pi 3 model B

言わずと知れた、Linux[1]が動作するシングルボードコンピュータの代表格です。
少し前に次世代機のRaspberry Pi 5が発表され、話題となりました。

Raspberry Pi 5
かっこいい

Raspberry Pi Pico (RP2040) も最近非常にアツいですね!

Raspberry Pi Pico
私もこの前一台購入しました

さて冒頭にお話した通り、私はこのRaspberry Piについて、慢性的に困っていることがありました。

Raspberry Piの煩雑さ

1. OSの準備

Raspberry Piの初期設定となると、基本的にはOSイメージを汎用ツールかRaspberry Pi ImagerでSDカードに書き込むことになるわけですが、特定のパッケージや環境をそろえた状態のイメージをつくるのはとても大変です。/boot/cmdline.txt/boot/config.txt を書き換えることは可能ですが [2]、それでもカスタマイズには限度があります。

CLIだけでいいのにGUIがついてきたり、最近はlite版であっても初回起動時にユーザアカウントを作成するためのインタフェースが立ち上がったり[3]というのも面倒です。

2. ソフトウェアの管理

Raspberry Pi上で動作する大規模なプログラムを開発する場合、 「PCで開発し、必要ならバージョン管理を行い、ビルド結果を適宜Raspberry Pi側に転送…」 という流れになるかと思います。

しかし、追加パッケージのインストール、ネットワークやミドルウェアの設定はOSイメージの書き込み後に手動で行わなければなりません。ディスプレイと自由に接続できるネットワークがあればさしたる問題にはなりませんが、常にそのような理想的な環境があるとも限りません(教育機関では自由な有線インターネット接続が提供されていない場合もあるでしょう)。

3. 運用

ホビーでちょっと試すだけ程度であればあまり気になりませんが、継続して稼働させようとなると無視できなくなってくるのが運用面です。プロジェクトおよび依存するパッケージ群のバージョンアップや動作テスト、そしてOS本体の更新など、本来悩まされる必要のない非本質的な作業が増加していきます。

作業量に伴って増加するのがケアレスミスです。
うっかりミスでOSから焼き直すハメに…という事故は、往々にして一番大事な時に起こります

もっとこうなったらいいのに

もっと、こう…

  • 依存パッケージを簡単に管理できて…
  • 本流のプロジェクトとも統合できて…
  • ビルドを走らせるだけでイメージが生成されるような…

かつ、それらを完全に自動化できる仕組みがあれば…

作った

ということで作りました

banner
Enchan1207/rpi-buildroot

組込みLinuxのビルドツール Buildroot を用いて Raspberry Pi のイメージを作成し、ビルドキャッシュを保存して結果を出力する GitHub Actions アクションです。
出力されたイメージファイルをSDに書き込み Raspberry Pi に挿入・起動すれば、プロジェクト動作環境の揃ったLinuxが起動します。

また、rpi-buildrootはアクションだけでなくローカルで使用可能なシェルスクリプト buildroot.sh (以降、起動スクリプトと呼称)も提供しています。このスクリプトはBuildrootをDockerコンテナ上で起動し、構成の編集をサポートするものです。
アクションの実行に必須ではありませんが、Buildrootの環境構築と構成ファイルの管理を自動で行ってくれるので便利です。

基本的な使い方

1. 事前準備

リポジトリをクローンします。

host
git clone https://github.com/Enchan1207/rpi-buildroot

起動スクリプトを実行します。

host
./buildroot.sh

イメージの確認と準備が行われます。

Docker image enchan1207/buildroot_base not found. pulling...
Using default tag: latest
latest: Pulling from enchan1207/buildroot_base
(省略)
Directory dist will be mounted to /dist.
While invoke make, please use instead:
    make O=/dist
root@551cbdfa53bb:/buildroot#

構成が完了すると、Buildrootの環境が整った状態のDockerコンテナが起動します。
ここではこれ以上の操作は行わないため、^D で抜けてしまって問題ありません(--rm で実行しているので、終了後にコンテナは消滅します)。

最後に、起動スクリプトを他の場所からも呼び出せるようにパスを通しておきます。

2. デフォルト構成ファイルの生成

プロジェクトディレクトリに移動します。

host
mkdir rpi_example_proj
cd rpi_example_proj
git init

後々 cache およびdist ディレクトリが生成されてしまうので、先にignoreしておきます。
(生成"されてしまう"と書きましたが、CIで動かす際はこれらを保存しておくことで高速化が可能です。注意事項で後述します。)

host
touch .gitignore
echo "cache" >> .gitignore
echo "dist" >> .gitignore

構成ファイルの名前を引数に渡して起動スクリプトを実行します。(スクリプト終了時、この名前でファイルが自動生成されます)

host
buildroot.sh raspberrypi.config

起動したシェルでこのコマンドを実行します。このコマンドにより[4]、Rasperry Pi 3のイメージに最低限必要な設定が記述された構成ファイルが /dist/.config に生成されます。

container
make O=/dist raspberrypi3_defconfig

構成ファイルの中身はこんな感じです:

/dist/.config
#
# Automatically generated file; DO NOT EDIT.
# Buildroot 2023.08.1 Configuration
#
BR2_HAVE_DOT_CONFIG=y
BR2_HOST_GCC_AT_LEAST_4_9=y
BR2_HOST_GCC_AT_LEAST_5=y
BR2_HOST_GCC_AT_LEAST_6=y
BR2_HOST_GCC_AT_LEAST_7=y
BR2_HOST_GCC_AT_LEAST_8=y
BR2_HOST_GCC_AT_LEAST_9=y

# (以下省略)

ベースとなる構成ファイルが完成したら、一旦コンテナを抜けます。

root@fcc9d8576807:/buildroot# exit
Copy generated config from /dist to ./dist.
exit

このタイミングで構成ファイルがプロジェクトディレクトリにコピーされるので、commitしておきます。

host
git add raspberrypi.config
git commit -m "[Add] config for raspberry pi image"

Buildrootはいい感じの構成エディタを提供しており、イメージのカスタムにはそれを使用するのですが、それについては構成ファイルの編集で後述します。ここではデフォルトの構成のまま次に進みます。

4. ワークフローの構成・Runnerの設定

次にワークフローを構成します。

.github/workflows/build_rpi.yml
on:
  push:
    branches: master

jobs:
  build_raspberry_pi_image:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: build Raspberry Pi image
        uses: Enchan1207/rpi-buildroot@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          config_file: raspberrypi.config
          output_path: sdcard.img
          buildroot_log_path: build.log

      - name: Upload built image as artifact
        uses: actions/upload-artifact@v3
        with:
          name: Built image and log
          path: |
            sdcard.img
            build.log

commitする前に、GitHub Actionsのランナー設定を変更します。
リポジトリの SettingsActionsGeneral へ進み、Workflow permissionsRead and write に変更します。

設定が完了したら、ローカルの変更をcommitしてpushします。

host
git add .github/workflows/build_rpi.yml
git commit -m "[Add] workflow"
git push origin master

イメージのビルドが開始されます。

actions

5. ビルドしたイメージの確認・起動

実際にハンズオン的に進めてくださっている方は実に長いこと待つことになってしまったかと思います。お疲れさまでした。
ビルド結果はアーティファクトとしてGitHubから確認できます。

artifact

これをダウンロードし、sdcard.imgを適当なツールで書き込みます。

file

今回は balenaEtcher を使用しました。

balenaEtcher

書き込みが完了したら、microSDカードを Raspberry Pi に挿して起動します。
デフォルトコンフィグで構成したイメージなら、10秒足らずでログイン画面に到達できます。root でログインできるはずです。

boot

自分だけの Raspberry Pi イメージを構成することができました! 🎉🎉🎉🎉

150MB程度 の小さなイメージで健気に動く Raspberry Pi 、とってもキュート じゃないですか?
かわいい…

基本的な使い方は以上です。

構成ファイルの編集

では、このイメージをどんどんカスタマイズしていきましょう。
Buildrootは構成ファイルのエディタとして以下の4種類を提供[6]しています。

  • TUI
    • menuconfig (cursesベース)
    • nconfig (ncursesベース)
  • GUI
    • xconfig (Qtベース)
    • gconfig (GTKベース)

今回はcursesベースの menuconfig を使用します。

host
buildroot.sh raspberrypi.config
container
make O=/dist menuconfig

構成エディタが開きます。

menuconfig

エディタの操作方法については長くなりすぎてしまうのでここでは省略します(操作感は古めのBIOSに似ています)。

構成ファイル編集時は以下の順番で操作を行います:

  1. 起動スクリプトを実行する (buildroot.sh raspberrypi.config)
  2. エディタを起動する (make O=/dist menuconfig)
  3. 構成を編集し、保存する
  4. コンテナから抜ける (exit または ^D)
  5. 変更をcommitし、pushする

応用例

パッケージを追加する

デフォルト(raspberrypi3_defconfig)で構成した Raspberry Pi は ルートでtreeしてもギリギリ読み切れるほど 小さく、ほとんど全てのコマンドはBusybox[7]により提供されています。
ここに新たなソフトウェアパッケージを追加していきましょう。

Target packages を選択すると、イメージに導入されるパッケージの管理画面に遷移します。

Games を選択すると、コンソールベースのゲームがいくつか表示されます。ascii-invadersを選択してビルドしてみます。

起動するとこんな感じ。 /usr/bin に実行ファイル ascii-invaders が追加されました。
実行するとタイトル画面が表示され、Spaceキーをタイプするとゲームが始まります。

方向キーで移動し、Spaceキーで弾を発射します。q をタイプするといつでも終了できます。

(ちなみに見た目より難しいです。)

手元のプロジェクトと統合する

やっと辿り着きました。ここが 本題 です。

Buildrootは生成されたファイルシステムをカスタマイズする機能をいくつか備えており[8]、そのうちの一つに Root filesystem overlays (ルートFSオーバーレイ) というものがあります。

Root filesystem overlays (BR2_ROOTFS_OVERLAY)

A filesystem overlay is a tree of files that is copied directly over the target filesystem after it has been built. To enable this feature, set config option BR2_ROOTFS_OVERLAY (in the System configuration menu) to the root of the overlay. You can even specify multiple overlays, space-separated. If you specify a relative path, it will be relative to the root of the Buildroot tree. Hidden directories of version control systems, like .git, .svn, .hg, etc., files called .empty and files ending in ~ are excluded from the copy.

ルートFS オーバーレイ (BR2_ROOTFS_OVERLAY)

ファイル システム オーバーレイは、ターゲット ファイル システムの構築後に直接コピーされるファイルのツリーです。 この機能を有効にするには、構成オプション BR2_ROOTFS_OVERLAY (システム構成 メニュー内) をオーバーレイのルートに設定します。 複数のオーバーレイをスペースで区切って指定することもできます。 相対パスを指定した場合、それは Buildroot ツリーのルートに対する相対パスになります。 .git.svn.hg などのバージョン管理システムの隠しディレクトリ、.empty と呼ばれるファイル、および ~ で終わるファイルはコピーから除外されます。

つまり、指定されたディレクトリ配下のファイルをビルド後のFSにコピーできる機能です。

rpi-buildrootはルートFSオーバーレイに対応しており、アクションの入力 rootfs_overlay_dir にパスを渡すことで、それをオーバーレイのルートディレクトリとして処理します。

つまり、たとえば:

workflow.yml
on: push

env:
    # ルートFSオーバーレイディレクトリの場所
    rootfs_overlay_dir: .github/workflows/buildroot_files/rfs

jobs:
  build_raspberrypi_image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # アプリケーションディレクトリ(/app)を作成し、リポジトリをそこにチェックアウト
      - name: checkout this repository in rootfs overlay directory
        uses: actions/checkout@v4
        with:
          path: ${{ env.rootfs_overlay_dir }}/app

      - name: build Raspberry Pi image
        uses: Enchan1207/rpi-buildroot@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          config_file: raspberrypi.config
          output_path: sdcard.img
          buildroot_log_path: build.log

          # ルートFSオーバーレイディレクトリを指定
          rootfs_overlay_dir: ${{ env.rootfs_overlay_dir }}

このように設定することで、リポジトリの内容を丸ごとRaspberry Piに含めることができます(この例では /app にリポジトリが展開されます)。

PythonやNode.jsなど、コンパイルの必要がない言語であればこれで統合できます。C/C++やGoなどのプロジェクトを動かす場合は、GitHub Actions上でトランスパイルしたのち、結果のみ rootfs_overlay_dir に渡すといった方法が考えられます。

Pythonの依存関係を管理する

「ルートFSっつってもよぉ!俺ぁ依存関係をたっぷり持つPythonプロジェクトをRaspberry Piで動かしてえんだよなぁ!」という声が聞こえてきました。

ご安心ください。

BuildrootならPythonも、その主要なパッケージも構成エディタから設定できます。

Target packagesInterpreter languages and scripting から、 python3を有効にします。

さらに、External python modules を選択します。

ここから必要なパッケージを選択すれば、それらがインストールされた状態のPython環境が使えるようになります。

その他

他にも、鍵ファイルを渡して起動直後からSSHを使えるようにしたり、X Windowを入れてGUIを用意したり、本当に色々なことができます。可能性は無限大です🔥

なお、ここで書ききれなかった応用例については、リポジトリのWiki に追加していく予定です。

注意事項

ビルド時間とキャッシュ

先述の通り、アクションの初回実行には1~2時間かかります。導入パッケージを増やしたり、BusyBoxの代わりにsystemdを使用したりするとその時間はさらに長くなります(私のケースでは2.5時間を超えることもありました)。

一回ビルドが最後まで通ると、アクションはディレクトリ cache (ccache[10]の出力先) および dist (Buildrootが使用) を GitHub Actions cache として保存します。このキャッシュが存在する場合、ビルド時間は最短で30分を下回る程度に短縮されます。また、ビルドキャッシュのほかBuildrootのベースイメージ(enchan1207/buildroot_base)も合わせてキャッシュされます。

ただ、このキャッシュはとても大きくなります。最低限の構成でも1.5GB以上のデータがキャッシュとして保存されます(工夫次第で小さく抑えることも可能ですが、ここでは割愛します)。

さらに、GitHub Actions cacheにはその使用量と保存期間に制限があります[11]。これを超過してキャッシュが削除されてしまうと、また最初からやり直し(=1~2時間コース)です。厳しい。

こちらについては、キャッシュを外部ストレージ(Google Drive等)に逃がす、またはそもそもキャッシュシステムを使用しないオプションを追加する等のアプローチを検討しています(現在のrpi-buildrootでは対応していません)。

無料枠と料金

publicリポジトリの場合は問題になりませんが、privateリポジトリではアクションの実行時間に 2,000分/月 の制限があります[12]
また、ビルドしたイメージのダウンロードは Storage for Actions and Packages の無料枠を消費してしまうらしく、あまり高頻度にビルドを回しすぎると GitHubからメールが来ます

現状有効な解決策としては、self-hosted runnerを使用するか、ビルド頻度を下げるか(リリース作成前、PRマージ直後など)…といった感じです。GitHub hosted runnerを使用している限り、完全に回避するのはちょっと大変そう。

既存ツールとの比較 📊

この記事を書くまで知らなかったのですが、似たような機能を提供するツールがいくつか存在しているようです:

https://github.com/RPi-Distro/pi-gen

https://github.com/Nature40/pimod

正直完全上位互換です(pimodに至ってはGitHub Actionsアクションも提供されています)。

…………まあ、もう作ってしまったので……

長所を上げるとすればこんな感じでしょうか:

  • 出力イメージを軽量にできる:
    既存のOSをベースとせず完全にゼロから構築しているため、プロジェクトに必要なプログラムのみが揃ったRaspberry Piを作ることが可能です。
    手元の環境では、相当遊んでも1GBを超えることはほとんどありませんでした。これはひとつのメリットかなと思います。
  • 構成を細かく編集できる:
    本文中ではパッケージとルートFSオーバーレイしか紹介できませんでしたが、Buildrootは他にも実に多くのオプションを提供しています。依存関係グラフを作成したり[13]、ビルド時間を分析したり[14]することも可能です。小さな箱庭を作る感覚で色々と試してみると楽しいかもしれません。
  • Raspberry Pi以外にも応用できる:
    Buildrootは beagleboneSTM32F429/439ZYNQなど様々な組込みLinuxのビルドに対応しているため、Raspberry Pi以外のイメージも構築できます。正直 これを言ってしまうとなんでもアリ な気もしますが…
    (rpi-buildroot という名前は、ツールチェーンファイルをリリースに同梱していた際の名残です。)

おわりに

ここまで読んでいただきありがとうございました。

正直 pi-genかpimod使えよ と言われてしまうとそれはそうなのですが、Buildrootは(ツールの力を借りているとはいえ)自分で組み立てている感があり、それはそれで(趣味として取り組む分には)多くの学びがありました。
研究等の場面で Raspberry Pi を連続稼働させることになりそうな方に届けば幸いです。

最後に、素敵なAdvent calendarを企画してくださった しゅん(@shun-shobon)さんに、この場をお借りして感謝いたします。本当にありがとうございました。

それでは。

fflush(stdout)

脚注
  1. 正確には Raspberry Pi OS ↩︎

  2. Raspberry Pi Documentation - The config.txt file ↩︎

  3. An update to Raspberry Pi OS Bullseye - Raspberry Pi ↩︎

  4. エネミーコントローラーCOMPLETE EDITION | KAIBA CORPORATION STORE ↩︎

  5. action.yml#L281-L295 ↩︎

  6. Buildroot quick start ↩︎

  7. BusyBox ↩︎

  8. Customizing the generated target filesystem ↩︎

  9. Infrastructure for Python packages ↩︎

  10. Ccache — Compiler cache ↩︎

  11. Caching dependencies to speed up workflows - GitHub Docs ↩︎

  12. GitHub Actions の課金について - GitHub Docs ↩︎

  13. Graphing the dependencies between packages - The Buildroot manual ↩︎

  14. Graphing the build duration - The Buildroot manual ↩︎

Discussion