
zerobrew はRust製のHomebrew代替。uv がpipを置き換えたのと同じノリでHomebrewを高速化しようというプロジェクトで、ウォームキャッシュなら7.6倍速いらしい。Homebrewも最近は速くなっていたけど、新しいものが気になって試してみた。結果的にめちゃくちゃ苦労した(まだexperimentalなのでこれからという感じ)。
まず最初の壁がprefixの長さ。zerobrewのデフォルトだと /opt/zerobrew/prefix にインストールされるんだけど、いきなりこう言われる。
error: store corruption: zerobrew prefix "/opt/zerobrew/prefix" (20 bytes) is longer than "/opt/homebrew" (13 bytes). On macOS, Mach-O binaries cannot be expanded in-place
macOSのMach-Oバイナリはライブラリパスが固定長で埋め込まれている。Homebrewのbottleは /opt/homebrew(13バイト) でビルドされているから、prefixはそれ以下でないといけない。/opt/zerobrew(13バイト) にしても一部のIntel向けbottleは /usr/local(10バイト) なので失敗する。安全策で /opt/zb(7バイト) まで詰めたら、今度は別の問題が起きた。
null-byteパッチングバグ
/opt/zb でインストールしたらtmuxが設定ファイルを読めなくなった。tmux -vvvv のログだと /opt/zb というディレクトリをコンフィグとして読もうとしている。hex dumpで調べた。
0009fdd0: 7420 6861 7665 2025 7300 2f6f 7074 2f7a t have %s./opt/z 0009fde0: 6200 0000 0000 002f 6574 632f 746d 7578 b....../etc/tmux 0009fdf0: 2e63 6f6e 663a 7e2f 2e74 6d75 782e 636f .conf:~/.tmux.co 0009fe00: 6e66 3a24 5844 475f 434f 4e46 4947 5f48 nf:$XDG_CONFIG_H 0009fe10: 4f4d 452f 746d 7578 2f74 6d75 782e 636f OME/tmux/tmux.co 0009fe20: 6e66 3a7e 2f2e 636f 6e66 6967 2f74 6d75 nf:~/.config/tmu 0009fe30: 782f 746d 7578 2e63 6f6e 6600 x/tmux.conf.
/opt/zb の直後に6バイトの \0 がある。元は /opt/homebrew/etc/tmux.conf:~/.tmux.conf:... という1本のC文字列だった。/opt/homebrew(13バイト) → /opt/zb(7バイト) の置換で余った6バイトをnull埋めした結果、文字列が分断されている。C文字列は \0 で終端されるから、tmuxには "/opt/zb" しか見えない。
zerobrewのパッチコードを見るとこう。
contents[i..i + new_bytes.len()].copy_from_slice(new_bytes); // /opt/zb を書き込み contents[i + new_bytes.len()..i + old_bytes.len()].fill(0); // 残り6バイトを\0埋め
prefixだけで独立した文字列なら安全だけど、/opt/homebrew/etc/tmux.conf:... のようにprefixがパスの先頭部分である場合は壊れる。tmux以外でも同じパターンを持つバイナリは全部壊れているはず。
@@HOMEBREW_PREFIX@@が残る
null-byteの件でprefixは元のHomebrewと同じ長さにすべきだとわかったので /opt/zerobrew に戻した。Homebrewのformulaは zb migrate の過程で消してしまっていたので、180個以上のformulaをBrewfileにまとめて zb bundle で入れ直した。
ところがneovimが起動しない。調べたらHomebrewのbottleにビルド時のプレースホルダー @@HOMEBREW_PREFIX@@ が残っていた。brew install ではここが実際のprefixに置換されるんだけど、zerobrewはこの置換をちゃんと実装していない。/opt/zerobrew/Cellar/ 以下で252ファイル、Mach-Oだけで312箇所にプレースホルダーが残っていた。neovim, git, pythonが軒並み壊れた。
install_name_tool と codesign で手動置換するワークアラウンドスクリプトを書いて対応した。
# Mach-Oの依存パスを置換
install_name_tool \
-change "@@HOMEBREW_PREFIX@@/opt/luv/lib/libluv.1.dylib" \
"/opt/zerobrew/opt/luv/lib/libluv.1.dylib" \
/opt/zerobrew/Cellar/neovim/0.11.6/bin/nvim
# 署名し直さないとmacOSにkillされる
codesign --force --sign - /opt/zerobrew/Cellar/neovim/0.11.6/bin/nvim
issue #209 で報告した。メンテナはパッチ方式の限界を認識していて "I have no interest in constantly patching an already subpar approach" とのこと。将来的に自前CDNかソースビルドへの移行を検討しているらしい。
ちなみにzb自体もHomebrewの liblzma に動的リンクされていて、Homebrewのformulaを消した瞬間にzbコマンドが死ぬ(issue #208)。自分がやろうとしていることの被害者になっている。
散々やり直した末に /opt/zerobrew + ワークアラウンドスクリプトでなんとか動いている。速度はめちゃくちゃ速い。ただ upgrade コマンドは未実装だし、パッケージを入れるたびにスクリプトを走らせるのはちょっとつらい。
興味がある人はHomebrewと併用するところから始めるのをおすすめします。