Mac使いのみなさん、homebrewは使っていますか?過去に こんな記事 を公開したこともあり、私は日常的にインストールするプログラムはCLI/GUI問わず、homebrewを使って brew install
でインストールすることにしています。
今回Homebrewのパッケージの定義ファイルである Formulaファイル を自作しようと思ったのですが、作成法がまとまったいい感じのページがなかったので、今回ハマったポイントも含めて知見を公開することにしました。
詳細に踏み込んでいるので記事は長めですが、このページだけで一通りかなり自由なFormulaを作ることができるようになるはずです。
誰向けなの?
- 自作コマンドを公開したい人
- tar ballは提供されているが、ダウンロードしてきて、
/etc/○○○.conf
ファイル書き換えて、.zshrc
に入れて・・・というインストール作業が面倒な人
自分で作ったプログラムをコマンドにして公開したい!という方はもちろんですが、後者の例のように、 公開されているプログラムがHomebrew対応しておらず、1台ごとにインストール手順を実行するのが面倒 、のようなケースも、Homebrewでは Formula
というスクリプトを書くだけで、様々なツールが簡単に brew install
できるようになります。
一度スクリプト化してしまえば複数台のMacをセットアップしたり、クリーンインストールする際にも使え、また公開・配布すれば他の人たちもハッピーになる(社内限の配布とかもできる)、やらない手はないのではないでしょうか。
今回Homebrew化するプロジェクト
今回 man
コマンドの内容を日本語に翻訳する man-pages-ja
という翻訳プロジェクトの配布物を brew install
できるようにしてみました。
日本語翻訳済みマニュアルファイルはこちらの JM Project で配布されています。
これをインストールして man
コマンドで日本語マニュアルを表示できるようにする方法はあるのですが、ステップが多くて面倒だった、というのが今回Homebrew化してみた動機です。
今回は上記手順で紹介されている手順を参考に、日本語版manコマンドを manj
というコマンドで呼び出すことができるようにします。
Formulaの作成
それでは早速Formulaを作っていきましょう。
基本は上記で紹介されている流れをそのままスクリプトに落としていくことになります。インストールに必要なステップは大きく分けて4つです。
- テンプレートの作成
- ファイル群をHomebrewのディレクトリにインストール
- manコマンドの設定ファイルを作成
- 日本語化されたmanjコマンドをインストール
1. テンプレートの作成
インストール対象となるコマンドが入った圧縮ファイルを指定し、 brew create
コマンドを実行します。 --set-name
ではインストール時に指定することになるFormulaの名前を指定します。
$ brew create --set-name=man-japanese http://linuxjm.osdn.jp/man-pages-ja-20220615.tar.gz
例えば自身のgithubレポジトリの中身を丸っとインストール対象にしたい場合は、 Code→Download ZIPからzipファイルのリンクを取得して指定することもできます。
すると、以下のようにFormulaのテンプレートが作成され、エディタが開きます。Formulaファイルはrubyスクリプトになっています。
# Documentation: https://docs.brew.sh/Formula-Cookbook
# https://rubydoc.brew.sh/Formula
# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!
class ManJapanese < Formula
desc ""
homepage ""
url "http://linuxjm.osdn.jp/man-pages-ja-20220615.tar.gz"
sha256 "8e965d065bd4f323600b8dd98145fd9ac5f1f49a648a4b9c086e0d5b2adbd1f3"
license ""
# depends_on "cmake" => :build
def install
# ENV.deparallelize # if your formula fails when building in parallel
# Remove unrecognized options if warned by configure
# https://rubydoc.brew.sh/Formula.html#std_configure_args-instance_method
system "./configure", *std_configure_args, "--disable-silent-rules"
# system "cmake", "-S", ".", "-B", "build", *std_cmake_args
end
test do
# `test do` will create, run in and delete a temporary directory.
#
# This test will fail and we won't accept that! For Homebrew/homebrew-core
# this will need to be a test that verifies the functionality of the
# software. Run the test with `brew test man-japanese`. Options passed
# to `brew install` such as `--HEAD` also need to be provided to `brew test`.
#
# The installed folder is not in the path, so use the entire path to any
# executables being tested: `system "#{bin}/program", "do", "something"`.
system "false"
end
end
基本的には、この install
メソッドの中にインストールコマンドを書いていきます。
2. ファイル群をHomebrewのディレクトリにインストール
圧縮ファイル man-pages-ja-20220615.tar.gz
の中身をHomebrewのディレクトリにインストールしていきます。
このパッケージには Makefile
が付属しているのですが、 make config
によってコマンドラインの入力による設定結果(installman.sh
)をもとにファイルをコピーしていく仕組みになっていました。
Homebrew化した際には、ユーザー入力を待たずにインストールさせたいので、インストール時の設定ファイル installman.sh
は予め用意しておいたものを別途ダウンロードし、インストールパスなど環境に依存する情報のみ実行時に渡す方針にします。
先程のテンプレートファイルの install
メソッド前後をこのように書き換えます。
...
license ""
resource "installman" do
url "https://raw.githubusercontent.com/sh0nk/homebrew-tap/main/manj/installman.sh"
sha256 "ac2dbf29ee50cf283c27470f0a6302e3cd0e7e5817dcc9e34c65f8f7f590ca9a"
end
def install
resource("installman").stage do
cp("installman.sh", buildpath)
end
manj_path = prefix/"manj"/"ja_JP.UTF-8"
mkdir_p manj_path
ENV["CELLAR_PATH"] = manj_path
system "bash", "-e", "installman.sh"
end
...
installman.sh
ファイルはあらかじめgithub上にアップロードしておいたので、そのファイルのハッシュ値(sha256)とURLを resource
ブロックに指定します。
その後、
resource("installman").stage do
cp("installman.sh", buildpath)
end
でこのファイルをダウンロードし、 cp
メソッドで buildpath
にコピーします。
ここで、buildpath
は brew install
実行時に作られるテンポラリディレクトリです。デフォルトだとインストール対象である man-pages-ja-20220615.tar.gz
が展開された状態になります。
この installman.sh
はインストール先パスを CELLAR_PATH
という変数で受け取るので、これを環境変数にセットし、 mkdir_p
でインストール先パスを作成、 bash
で installman.sh
を実行します。
# インストール先ディレクトリ
manj_path = prefix/"manj"/"ja_JP.UTF-8"
# ディレクトリを作成
mkdir_p manj_path
# CELLAR_PATHという名前でインストール先ディレクトリを渡す
ENV["CELLAR_PATH"] = manj_path
# bashでスクリプトを実行
system "bash", "-e", "installman.sh"
prefix
は今回の man-japanese
パッケージのインストール先ディレクトリを示す変数です。具体的には #{HOMEBREW_PREFIX}/Cellar/#{name}/#{version}
のように展開されます。
ちなみに Cellar
はワインセラーのセラーで、貯蔵庫を指します。Homebrewが 自家醸造酒 を意味するので、様々な用語が醸造に関連する用語になっています。
その他にも Formula
は(より正確な意味での)レシピ・製法を意味します。これらの用語は、公式ドキュメントの冒頭で説明されています。
先程の buildpath
や prefix
などその他様々な関連ディレクトリがデフォルトで変数化され、用意されています。 その他のコマンドは、 こちらの公式ドキュメント を参考にしてください。
3. manコマンドの設定ファイルを作成
manはgroffという文書整形を行うコマンドを利用してマニュアルを表示しています。しかし、Macにデフォルトでインストールされているgroffコマンドでは日本語翻訳ファイルが扱えないので、Homebrewで新しいバージョンをインストールするよう依存を張ります。
man-japanese.rb
のlicenseの下辺りに、 depends_on "groff"
を追加するだけで、manjコマンドインストール時にgroffもインストールしてくれます。
license ""
depends_on "groff"
manコマンドの設定ファイルは、通常 /etc/man.conf
が利用されますが、 -C file_path
と引数で渡してあげることで別の場所の設定ファイルを読み込むことが出来ます。
ここでは、デフォルトの /etc/man.conf
を buildpath
下にコピーし、 manj
用に書き換えます。
man-japanese.rb
のinstallメソッドの続きに、以下を追加します。
...
cp("/etc/man.conf", buildpath/"manj.conf")
inreplace "manj.conf" do |s|
s.gsub!(/^JNROFF.+$/, "JNROFF #{Formula["groff"].opt_bin}/groff -Dutf8 -Tutf8 -mandoc -mja -E")
s.gsub!(/^PAGER.+$/, "PAGER /usr/bin/less -isr")
s.gsub!(/^BROWSER.+$/, "BROWSER /usr/bin/less -isr")
end
etc.install "manj.conf"
...
inplace
ブロック は、ファイルの中身を置換するためのブロックです。
デフォルトの設定だと、今回インストールする翻訳ファイルに gsub
メソッドを使って、 JNROFF
, PAGER
, BROWSER
で始まる行をそれぞれ置換します。
最後に、Homebrewの管理している etc
ディレクトリに作成した manj.conf
をインストールしています。
4. 日本語化されたmanjコマンドをインストール
最後に、 manj
コマンドをHomebrew管理化の bin
ディレクトリにインストールします。
-
LANG
変数でman
コマンドの立ち上がる言語を切り替えられる -
MANPATH
変数にインストールしたmanファイルへのパスを通す -
-C
オプションで任意の設定ファイルを渡せる
上記の知識を利用して、簡単なwrapperスクリプトを準備して完了です。
man-japanese.rb
のinstallメソッドの続きに、以下を追加します。
(buildpath/"manj").write <<~EOS
#!/bin/sh
env LANG=ja_JP.UTF-8 MANPATH=#{manj_path.to_s}:$MANPATH man -C #{etc.to_s}/manj.conf $@
EOS
bin.install "manj"
デバッグ
それでは、作成したFormulaファイル man-japanese.rb
が動くか試してみましょう。この時点ではまだgitのレポジトリにアップロードする必要はありません。
以下のように、 brew install
に作成したファイルをそのまま渡します。
$ brew install --build-from-source --debug Formula/man-japanese.rb
ここで --debug
オプションを渡しておくと便利です。例えば、途中のインストールコマンドにtypo (bash installman.s
hが抜けている) があった場合、エラーになった箇所で処理が一時停止し、 irb
を立ち上げたり、 shell
を起動することが可能です。特に shell
を起動した場合、 buildpath
の生成物が見られるのでデバッグが捗ります。
/opt/homebrew/Library/Homebrew/ignorable.rb:29:in `block in raise'
BuildError: Failed executing: bash -e installman.s
1. raise
2. ignore
3. backtrace
4. irb
5. shell
公開
デバッグが完了したら、作成したFormulaを公開していきます。
公開は、Homebrew Tap という仕組みを使います。 (Tapは注ぎ口を意味し、ビールなどでいうサーバーです)
Homebrew Tapを使うと、Github上にアップロードされたFormulaを元に brew install
することができます。
HomebrewのFormulaは、複数を1レポジトリで扱うことが可能です。また、レポジトリ名には決まりがあり、 homebrew-*
という命名にする必要があります。今回は homebrew-tap
という名前にしました。以下のようなディレクトリ構成にし、Githubにpublicレポジトリとしてpushします。
sh0nk/homebrew-tap
.
└── Formula
└── man-japanese.rb
すると、
$ brew install sh0nk/tap/man-japanese
というコマンドでインストールできるようになります。 (tap
の部分は先程の homebrew-*
の *
に対応)
ちなみにURLをフルで指定すれば、Githubじゃない場所からもインストールすることが可能です。
今回公開したレポジトリを参考に置いておきます。
tapを利用せずにインストールさせたい場合は、 homebrew-core
に対してpull requestを送って取り込んでもらうことで可能になります。その際は いくつかのルール を満たす必要があります。
こうやってみればわかるのですが、Homebrew Tapの仕組みを使うと、野良のレポジトリに入ったよくわからないものをインストールしてしまう危険性があります。その人のGithubレポジトリのFormulaを経由してマルウェアを仕込むようなことも可能になって来ますので、インストールする側の自衛として、Formulaを読めるようになっておくこともHomebrew Formulaを学ぶ一つのメリットと言えます。
Tips
Homebrewでは、Formulaで使えるruby文法のAPIが用意されています。
しかしAPIドキュメントだとさすがに細かすぎるかもしれません。実際のFormula作成時には、homebrew-coreに取り込まれているFormulaを参考にしながら書いていくと、推奨の書き方も理解できるので良いでしょう。
参考文献
- https://www.rasukarusan.com/entry/2019/11/03/211338
- https://zenn.dev/hisasann/articles/a548c849a906e9614a6b
- https://deeeet.com/writing/2014/05/20/brew-tap/
おまけ
今回作った manj
コマンドを活用してみる記事はこちらに書きましたので是非合わせてご賞味ください。