Homebrew に毎日どんなパッケージが追加されたか追跡する

(記事は中国語) 中文

Homebrew の公式には「毎日新規追加パッケージ」ページがない。GitHub tree API で隣接する二日のディレクトリファイルリストを比較し、さらに LLM で説明を翻訳して、毎日自動更新する追跡ツールを構築した。


Homebrew を使う人なら誰もが気になることがある。毎日一体どんなパッケージが追加されているのか。Homebrew の公式は formulae.brew.sh ページを提供しており、現在利用可能なすべての formula と cask を列挙している。八千以上のパッケージがアルファベット順に並ぶ。しかしこれは現在のスナップショットを与えるだけで、「今日何が新しく追加されたか」というビューはない。GitHub 上の homebrew-core リポジトリには完全なコミット履歴がある。八十万回以上のコミットがあるが、新規パッケージを日付で絞り込むインターフェースはない。

この穴を埋めたい。毎日自動更新されるページを作り、説明も日本語に翻訳したい。

第一歩はデータソースの選定だ。formulae.brew.sh の JSON API は現在の全パッケージのメタデータしか返さず、履歴スナップショットはない。「新規」を出すには二日間の差分を比較する必要がある。最も直感的な方法は homebrew-core リポジトリをまるごと clone し、git log --diff-filter=A --since --until で指定日の新規ファイルを調べることだ。しかし homebrew-core は八十万回以上のコミットがあり、完全に clone すると 1GB を超える。私のサーバーはメモリ 1G、CPU 1コアで、clone プロセス中にマシンが直接クラッシュした。

一歩引いて考える。完全な履歴は必要ない。ある日にリポジトリにどんな .rb ファイルがあるかを知り、前日と比較すれば、増えた分が新規追加だ。GitHub の tree API がまさにこれを行う。commit sha を渡し、recursive=1 パラメータを付けると、その時点のリポジトリ内の全ファイルパスのリストを一度に返す。上限は十万エントリまたは 7MB で、homebrew-core の Formula ディレクトリは八千五百以上のファイルがあり、上限には程遠い。

具体的な手順は三つに分かれる。まず GitHub commits API で隣接する二日の最後の commit の sha をそれぞれ取得する。次にこの二つの sha でそれぞれ tree を取得し、Formula/Casks/ ディレクトリ下の .rb ファイル名を抽出する。二つの集合で差分を取ると、増えた分が当日の新規パッケージ名だ。

パッケージ名を取得したら、説明、ホームページ、バージョンなどのメタデータは formulae.brew.sh の現在の API で調べる。新規パッケージは現在のリストに残っている可能性が高く、見つからないものはスキップする。最後に英語の説明をバッチで LLM に送って日本語に翻訳し、固有名詞とコマンド名の原文を保持する。DeepSeek V4 Flash を使い、四十個の説明を一つのリクエストにまとめ、JSON マッピングで返す。数秒で処理が完了する。

翻訳には一つ注意すべき詳細がある。プロンプトで「JSON オブジェクトのみを返し、キーを name、値を日本語の翻訳とし、いかなる説明も加えない」と明記する必要がある。そうでないとモデルが「以下が翻訳結果です」のような前置きを付け、JSON パースが直接失敗する。制約を加えても、時折フォーマットの揺らぎに遭遇する。スクリプトにはフォールバックがあり、パース失敗時は英語の原文に戻し、一つの翻訳エラーで全体のフローを中断しない。

全体のフローは二つのスクリプトに分けた。一つは当日分を実行し、昨日と今日の tree を比較して当日のデータを出力する。もう一つは日付パラメータを受け取り、任意の歴史日付をバックフィルできる。原理は同じだ。出力は日付別に整理された JSON ファイルとインデックスだ。ページは純粋な静的 HTML で、クライアント側で JSON を読み込んでレンダリングし、日付セレクターで異なる日付のデータを切り替える。

定期タスクは cron を使う。毎日 06:00 UTC に当日スクリプトを一回実行し、結果はサーバー上の静的ディレクトリに直接書き込む。data ディレクトリは gitignore で無視され、データはサーバーローカルにのみ存在し、ページ自体は git に含まれる。これにより git 履歴が清潔に保たれ、データは毎日自動的にローリング更新される。

途中で非常に実のあるトラブルに見舞われた。サーバー上で SSH で git clone を試した際、コマンドがタイムアウトした後、ローカルターミナルに「Command aborted」と表示されたので、プロセスは停止したと思い込んだ。実際には SSH のタイムアウトはローカル接続を切断するだけで、リモートのプロセスはまだ動いていた。あの git index-pack がずっとバックグラウンドで 42% の CPU を食い続け、1G1C のサーバーのリソースを使い果たし、ブログサービスが繰り返しクラッシュと再起動を繰り返した。調査時に systemctl のログで fanshikun サービスが数分おきに exit し、restart counter が十七回に達しているのを見て、バックグラウンドの clone が kill されていないことに気づいた。

教訓は直接的だ。SSH で長時間タスクを走らせる場合は timeout でラップするか、タイムアウト後に能動的に ssh hkl 'pkill -f ...' でリモートプロセスをクリーンアップする必要がある。ローカルターミナルの「aborted」だけを見るとリモートの状態を誤判断する。1G1C のマシンで clone のような重い作業を走らせるべきではなく、データソースの作業は API に任せ、clone はローカルの mac に残すべきだ。

ツールの公開後、ページは毎朝一回更新される。歴史日付のバックフィルはサーバーで一行のコマンドを実行するだけで、数秒で結果が出る。六月二十一日に新規パッケージは三つあった。一つは GitHub Actions の言語サーバー、一つは 8BitDo コントローラーのファームウェアアップデーター、一つは Palmier Pro という AI 動画エディターだ。翻訳の品質は良好で、「Language server for GitHub Actions YAML files」は「GitHub Actions YAML ファイルの言語サーバー」と訳され、固有名詞はすべて保持されている。

この手法にはいくつか既知の制限がある。tree API はファイルの新規追加と削除しか検出できず、リネームを識別できない。あるパッケージが Formula/a/axxx.rb から Formula/b/bxxx.rb に改名されると、一回の削除と一回の新規追加としてカウントされる。移行も識別できず、パッケージが cask から formula に移行するとディレクトリをまたいで移動し、同様に二回の独立した変化としてカウントされる。もう一つのケースは同日内に新規追加されてから削除された場合で、たまたまその日の最後のコミット時にもう存在しないと、完全に見つからない。これらのエッジケースは数が少なく、このツールの用途には許容できる。

このツール自体に技術的な難易度はない。核心は formulae.brew.sh に履歴ビューがないことを発見し、GitHub tree API でその穴を埋め、LLM 翻訳をフローの最後に繋げたことだ。本当に時間がかかったのはデータソースの選定と、あの SSH プロセスのトラブルだ。ツールを完成させてから、毎日ページを開いてざっと見るだけで、普段なら気づかない面白いプロジェクトをいくつか発見できる。


関連記事: