平常運転

アニソンが好き

過去記事とかは記事一覧で見れます

Homebrew Formula の CI を GitHub Actions で行う

ビールの話ではなくてパッケージマネージャの話です。

主に macOS 向け*1のパッケージマネージャ Homebrew の Formula のテストをする話だったり、特に GitHub Actions で CI する仕組みを少し調べてみました、という話をします。

このエントリははてなエンジニア Advent Calendar 2021の19日目です。昨日は id:onk さんの ブログから技術記事を抽出・集約してワイワイする - id:onk のはてなブログでした。

qiita.com

ちなみに、本稿ではさも我が物のように Mackerel の Homebrew Tap を取り上げており、実際今年の夏までは Mackerel の開発チームにいたのでそんなに間違っていなかったのですが、夏以降は別のプロダクトの開発チームにいるので今は Mackerel の開発当事者ではなくなっているということは念のため付記しておきます。なのでこのエントリの中身は全て趣味です。勝手にネタにしてごめんね! > Mackerel チームのみんな

Homebrew の Tap

始めに Homebrew の Formula (や、それを束ねた Tap)とはなんなのかを簡単に確認しておく。

Formula は Homebrew におけるパッケージ定義。だいたい実物のファイルを見ると想像がつくけど、あるソフトウェアをビルドする方法であったり、ビルド済みのバイナリ (Bottle) の URL などが定義されていて、中身は RubyDSL で書かれている。

github.com

Homebrew の Formula は 中央のリポジトリでホストされているけど、この中央で管理されている Formula の他にサードパーティリポジトリを提供するための仕組みもあって、それが Tap。

docs.brew.sh

例えば AWS が自社便利グッズの Tap を公開していたり、

github.com

あるいは Mackerel でも、 macOS 向けに mackerel-agent や mkr をインストールするための Tap を公開している。

github.com

Tap の実態はシンプルで、 homebrew- で始まる名前のリポジトリの中に Formula ファイルが置いてあればヨシ!というもの。だいたいは Homebrew の公式ドキュメントに書いてある。

docs.brew.sh

Formula のテストをする

RubyDSL をよしなに書けば Formula は作れるし、それを先述のように適当なリポジトリの所定のディレクトリ構成に置いておけば Tap も作れ、自作リポジトリを自分や他人が使えるようにすることができる。しかしながら、当然 Formula を書くからには、書いた Formula が正しく動いているか試したくなるはず。あるいは機能的な要件は満たせたとして、それがお行儀良い Formula を書けているか検証したくなるだろう。そのへんもだいたいドキュメントに書かれている。

docs.brew.sh

特にテストについては Add a test to the formula の節にあり、スタイルガイドを満たしているかの確認については Audit the formulua の節にある。要は:

  • Formula#test に、正しくインストールできてたら成功するはずのコマンドを書き並べ、brew test $(formula) で実行しましょう
  • brew audit --strict --online $(formula) でスタイルガイドを満たしているか確認できます

上記のドキュメントはどちらかというと中央のリポジトリに Contribute する前提で書かれていそうだけど、自分のリポジトリで Tap として提供する場合もこれを満たせるとよさそう。(audit については議論の余地があるかもしれない。末尾でちょっと話を蒸し返すことにする。)

脱線: brew audit はどう実行するか

brew の Formula の検証系のコマンドには、上述の brew test brew audit と、後 audit の中で透過的に実行される brew style などがある。これらをはじめとする brew のサブコマンドの多くは手元の Formula を対象にして実行することができる。無限にあるけどいくつか例示するとこういう感じ:

$ brew unlink mkr # もともとの mkr を一旦 $(brew --prefix)bin から取り除いて
$ brew style ./mkr.rb
$ brew install --formula ./mkr.rb
$ brew test --formula ./mkr.rb
$ brew uninstall --formula ./mkr.rb
$ brew link mkr # もとの mkr に戻す

ところが、 brew audit はこういう野良の Formula に対して実行することは想定されていなくてエラーになる。

$ brew audit --formula ./mkr.rb
Error: undefined method `path' for nil:NilClass
Did you mean?  paths
Please report this issue:
  https://docs.brew.sh/Troubleshooting
/opt/homebrew/Library/Homebrew/formula_auditor.rb:124:in `audit_synced_versions_formulae'
/opt/homebrew/Library/Homebrew/formula_auditor.rb:823:in `block in audit'
/opt/homebrew/Library/Homebrew/formula_auditor.rb:818:in `each'
/opt/homebrew/Library/Homebrew/formula_auditor.rb:818:in `audit'
/opt/homebrew/Library/Homebrew/dev-cmd/audit.rb:185:in `block in audit'
/opt/homebrew/Library/Homebrew/dev-cmd/audit.rb:169:in `map'
/opt/homebrew/Library/Homebrew/dev-cmd/audit.rb:169:in `audit'
/opt/homebrew/Library/Homebrew/brew.rb:110:in `<main>'

brew audit の場合は、brew tap してインストールされる$(brew --repo mackerelio/mackerel-agent) 、具体的には /opt/homebrew/Library/Taps/mackerelio/homebrew-mackerel-agent みたいな場所に配置しないといけない。これはこういうものらしい。

github.com

CI する

ということでやればいいのだけど、最初に Formula を書くときならいざ知らず、一旦書き上げた後に Formula のテストを手元でガチャガチャやるのは往々にして面倒だったりする。また、一般的にテストや lint は CI を整備しなければあっという間に滅びていくことが知られている[要出展]。

ということで GitHub Actions で CI する。 GitHub Actions は macOS も使えるし Public リポジトリなら無料なので都合が良い。あと設定ファイルとかが GitHubリポジトリの中で完結するのも良い。Apple Silicon の実行環境がないので、本格的な Formula の場合は困ることがあるかもしれないというのは惜しい。
また、 Homebrew に関しては公式で便利な Action が公開されていて、これを使うことで CI 環境への Homebrew のセットアップをガッとやることができる。

github.com

基本的にはこれで Homebrew を CI 環境の Mac の中にセットアップして、あとは brew install & brew testなり brew auditなりを実行すればよい。

ということで突然なんだけど完成品です。めっちゃ CI が落ちてる話は後でフォローするのでまだ慌てないでほしくて、とはいえ先回りしてフォローしておくと Mackerel の Homebrew Tap は機能的には問題なく動作する。テストの内容が古びているとか、普段使わない brew install --HEAD の手順が古びているとかそういう系で怒られているのが主。

github.com

ワークフローの定義はそんなに複雑な物ではないはず。先述した Homebrew/actions/setup-homebrew は、対象のリポジトリを tap として $(brew --repo astj/mackerel-agent) などのよしななところに勝手に clone してくれるので、後続の brew コマンドは brew install astj/mackerel-agent/mkrみたいに、インストール済みの Tap としてコマンドをあれこれ実行することができる。こうすることで、先述した brew audit もトラップにはまらずに実行できる。そして GitHub Actions で普段お世話になる actions/checkout は登場していない、ということになる。

今回は試しにということでモリモリで用意していて、通常のユーザー向けと同じ手順で brew install してからテストするもの、 brew audit でスタイルチェックするもの、あと brew install --HEAD で最新リビジョンのインストールするものの3通りを CI している。一般的にはソースコードからビルドすると時間がかかるので、場合によってはコミットごとに毎回実行してられないよ〜となるケースもあるかもしれない。その辺は実際のリポジトリのライフサイクルに応じてビルドトリガを調整するといいと思うし、例えば HEAD のインストールをちゃんとテストしたいのであれば Formula の変更をトリガにするだけでは不足で、定期的に実行することで Upstream の変更に引きずられて勝手に壊れていた、ということも防げるだろう。

ちなみに audit の怒られは GitHubGUI 上で確認できる。 audit の中身が RuboCop だからよしなに表示できてるのかもしれないし、もしかしたら Homebrew の action が頑張っているのかもしれない(未確認)。

f:id:astj:20211210023349p:plain
コミットの画面で怒られを確認できる様子

余談: go@1.15 をたおせ!

ワークフローの中身についてもうちょっと見所を挙げるとするなら HEAD のインストールのワークフローの中で、明示的に Go をインストールしているところだろうか。

      # there may be go@1.x installed and it conflicts with go
      - name: Preinstall go
        run: brew install go || brew link --overwrite go

今回対象にした Formula はどちらも HEAD からインストールするときに brewgo Formula をインストールしようとする。ところが、GitHub Actions の macOS 環境には最初から Homebrew で Go が入っているのだけど、2021/12現在の macos-latest では go ではなくて go@1.15 が入っているので、成り行きに任せると競合して失敗してしまう。仕方が無いので予め brew link --overwrite go して go がインストールされた状態を用意している。Go に依存しない Formula ではこの step は必要ないけど、逆に依存する Formula で思い出すと役に立つことがあるかもしれない。。

f:id:astj:20211210023037p:plain
go の競合で失敗していた様子

CI を通す

ということで CI は書けたんだけど、今回のケースだと CI がめっちゃ落ちている。念のためもう1回予防線を張っておくと、 Mackerel の Homebrew Tap は通常のユースケースでは全く問題なく動く。brew install --HEAD でのインストールは多分長年誰にも実行されていないし、brew test のコマンドが正しく実行されないのも通常の Homebrew のユースケースでは実行されないから気付いていなかっただけなので、機能提供には実害はない。
ないんだけど、気付いてしまったら後は直せば良いだけである。幸い CI があるので直ったことの確認は容易。このエントリを公開するまでに Upstream の mackerelio/homebrew-mackerel-agent に修正を持って行けるかは分からないけど、ひとまず fork 先ではもう直している。

github.com

audit を蒸し返す

さきほどの修正の差分を見てもらうと分かるんだけど、今回の差分の見た目上の変更は brew audit 経由で呼ばれた RuboCop によって quote を single quote から double quote に直すことを強いられているのが大半を占めている。ちなみにこの書き替え自体は brew audit --fix ./mkr.rb とかでだいたいやってくれる。で、ここまで audit しようぜという話をしておいて話をひっくり返すんだけど、 Tap においての audit はまあ必ずしも通さなくてもいいんじゃないかな〜という感想も持っている。勿論 Homebrew の中央リポジトリに登録するのであればスタイルガイドに従う必要があるだろうけど、 Tap であれば自分の城なので、例えば自分流の RuboCop スタイルとかがあるのならそちらに従う方がトータルの精神衛生は良い場合もあると思う。今回のケースでいうと quote はちょっとぎょっとする、他はまあ妥当だと思う、という感想を持った。今回書いたワークフローを Upstream に持って行くとして、 audit を持って行くかはまだ迷っている、というのが正直なところでもある。

ハッピーブリューイング!

ここまで Homebrew の Formula の CI の話をしました。 GitHub Actions の太っ腹さと Homebrew の公式 action の便利さのおかげで実際の CI の実現は簡単なので、もしお手元にテストされてない Tap があるなら CI を追加して、久々に Formula をじっくり眺めてみるといいかもしれません。

※ アルコール度数が1%を越えて酒類となるビールの自家醸造は違法です。
www.nta.go.jp

アドベントカレンダーの明日の担当は id:Pasta-K さんです!

*1:Linux でも使えることは使えますが、ほとんどのケースでは macOS で使われているでしょう……