投稿

自作ツールを brew install できるようにするまでにやったこと

自作ツールを brew install できるようにするまでにやったこと

まとめ

  • 個人 tap(自分の GitHub リポジトリ)方式なら、審査なしで push した瞬間から brew install できる
  • GoReleaser の brews(formula 生成)は非推奨になっていて、今は homebrew_casks を使う
  • 配布そのものより、public 化前の掃除(ライセンス・git 履歴の author 書き換え)の方が手間だった
  • cask は brew services が使えないので、常駐はツール側に launchd インストーラを持たせた

何を配布したかったか

ローカルの Markdown をブラウザでレビューする自作ツール。Go サーバに React SPA を go:embed した単一バイナリと、CLI の 2 本構成。ずっと private リポジトリ + make build で使っていたが、public にして brew install 一発で入るようにした。

public 化の前にやった掃除

コードより先にこっちが本題だった。

  • LICENSE がなかったので MIT を追加
  • テストフィクスチャやドキュメントに入っていた個人名・手元のパスを汎用化
  • git 履歴の author が個人メールだったので、git filter-repo で GitHub の noreply アドレスに書き換え

履歴の書き換えはこんな感じ。mailmap で author/committer、--replace-message でコミット本文の Co-authored-by: トレーラも一緒に直せる。

git clone --bare git@github.com:OWNER/REPO.git
cd REPO.git
git filter-repo \
  --mailmap mailmap.txt \
  --replace-message replace.txt
git push --force --all origin

1点注意。force push しても、GitHub 上では古い SHA のコミットがしばらく参照できる(古い PR ページや commit/SHA.patch 経由)。完全に消したければ GitHub Support に GC を頼むしかない。

GoReleaser + 個人 tap

配布は GoReleaser に全部任せた。v* タグを push すると GitHub Actions が動いて、

  1. フロントエンドをビルドして Go バイナリに embed(before hook で npm ci && npm run build
  2. darwin/linux × amd64/arm64 の 4 バイナリをクロスコンパイル
  3. GitHub Releases にアーカイブを公開
  4. 自分の tap リポジトリに Cask 定義を push

までが自動で終わる。SQLite を pure-Go 実装にしていたおかげで CGO なしのクロスコンパイルが素直に通った。ここで CGO が要ると急に面倒になるはず。

ハマりかけたのが GoReleaser の Homebrew 対応。昔からある brews(formula 生成)は deprecated で、今は homebrew_casks が推奨。バイナリ配布に formula を使うのがそもそもハックだった、という整理らしい。未署名バイナリなので Gatekeeper 対策の hook も入れる。

homebrew_casks:
  - name: mytool
    repository:
      owner: OWNER
      name: homebrew-tap
      token: ""
    binaries: [mytool-server, mytool-cli]
    hooks:
      post:
        install: |
          if OS.mac?
            system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/mytool-server"]
          end

tap への push 用に、tap リポジトリだけに contents:write を持たせた fine-grained PAT を Actions の secret に登録しておく。

tap は登録した瞬間から入る

一番意外だったのがここ。リリースが走り終わった直後に brew install OWNER/tap/mytool が通って、「そんなにすぐインストールできる状態になるものなの?」と思わず聞き返した。

からくりは単純で、tap はただの git リポジトリ。OWNER/tap/ プレフィックス付きで install すると、brew がそのリポジトリを clone して中の Cask を読むだけ。homebrew-core と違って審査も notability 要件もない。個人ツールの配布はこれで十分。

常駐だけはひと工夫いる

cask は formula と違って brew services に対応していない。macOS で常駐させたかったので、サーバ自身に launchd インストーラを持たせた。

mytool-server service install --root ~/notes
mytool-server service status
mytool-server service uninstall

地味に大事なのが plist に書くバイナリパス。os.Executable() で解決すると Caskroom のバージョン付き実パスに固定されてしまい、brew upgrade で壊れる。argv[0] 由来の /opt/homebrew/bin/ の symlink を保持するようにした。

あと、ルート指定を最初 --review-roots '[{"name":...,"path":...}]' みたいな JSON フラグにしたら、自分で使ってみて設定しづらすぎた。すぐ --root name=path を繰り返し指定する形に直した。ドッグフーディングはやるものだ。

更新フロー

リリースは git tag v0.2.0 && git push origin v0.2.0 だけ。利用者側は普通に brew upgrade。常駐している場合はプロセスの再起動(launchctl kickstart か、引数なしの service install で設定を引き継いで再ロード)が要る。

半日仕事のつもりが履歴の掃除まで含めて丸一日かかった。それでも「clone して make して」と口頭で説明していた頃と比べると、配布の楽さは別物になった。

トレンドのタグ