はじめての自作Homebrew Formula

Mac使いのみなさん、homebrewは使っていますか？過去に [こんな記事](https://qiita.com/sh0nk/items/f88baa508746f06f30ed) を公開したこともあり、私は日常的にインストールするプログラムはCLI/GUI問わず、homebrewを使って `brew install` でインストールすることにしています。

今回Homebrewのパッケージの定義ファイルである **Formulaファイル** を自作しようと思ったのですが、作成法がまとまったいい感じのページがなかったので、今回ハマったポイントも含めて知見を公開することにしました。

詳細に踏み込んでいるので記事は長めですが、このページだけで一通りかなり自由なFormulaを作ることができるようになるはずです。


# 誰向けなの？

- 自作コマンドを公開したい人
- tar ballは提供されているが、ダウンロードしてきて、`/etc/○○○.conf` ファイル書き換えて、`.zshrc` に入れて・・・というインストール作業が面倒な人

自分で作ったプログラムをコマンドにして公開したい！という方はもちろんですが、後者の例のように、 **公開されているプログラムがHomebrew対応しておらず、１台ごとにインストール手順を実行するのが面倒** 、のようなケースも、Homebrewでは `Formula` というスクリプトを書くだけで、様々なツールが簡単に `brew install` できるようになります。

一度スクリプト化してしまえば複数台のMacをセットアップしたり、クリーンインストールする際にも使え、また公開・配布すれば他の人たちもハッピーになる（社内限の配布とかもできる）、やらない手はないのではないでしょうか。


# 今回Homebrew化するプロジェクト

今回 `man` コマンドの内容を日本語に翻訳する `man-pages-ja` という翻訳プロジェクトの配布物を `brew install` できるようにしてみました。
日本語翻訳済みマニュアルファイルはこちらの JM Project で配布されています。

https://linuxjm.osdn.jp/

これをインストールして `man` コマンドで日本語マニュアルを表示できるようにする方法はあるのですが、ステップが多くて面倒だった、というのが今回Homebrew化してみた動機です。

https://zenn.dev/kumamoto/articles/d536ac6df8a544

今回は上記手順で紹介されている手順を参考に、日本語版manコマンドを `manj` というコマンドで呼び出すことができるようにします。


# Formulaの作成

それでは早速Formulaを作っていきましょう。

基本は上記で紹介されている流れをそのままスクリプトに落としていくことになります。インストールに必要なステップは大きく分けて４つです。

1. テンプレートの作成
2. ファイル群をHomebrewのディレクトリにインストール
3. manコマンドの設定ファイルを作成
4. 日本語化されたmanjコマンドをインストール


## 1. テンプレートの作成

インストール対象となるコマンドが入った圧縮ファイルを指定し、 `brew create` コマンドを実行します。 `--set-name` ではインストール時に指定することになるFormulaの名前を指定します。

```bash:shell
$ brew create --set-name=man-japanese http://linuxjm.osdn.jp/man-pages-ja-20220615.tar.gz
```

例えば自身のgithubレポジトリの中身を丸っとインストール対象にしたい場合は、 Code→Download ZIPからzipファイルのリンクを取得して指定することもできます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29632/b8596a58-420e-5811-9500-75cdfbc82a7c.png" width=300px >

すると、以下のようにFormulaのテンプレートが作成され、エディタが開きます。Formulaファイルはrubyスクリプトになっています。

```ruby:man-japanese.rb
# 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` メソッド前後をこのように書き換えます。

```ruby:man-japanese.rb
  ...
  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` ブロックに指定します。

その後、

```ruby
    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` を実行します。

```ruby
    # インストール先ディレクトリ
    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}` のように展開されます。

:::note info
ちなみに `Cellar` はワインセラーのセラーで、貯蔵庫を指します。Homebrewが **自家醸造酒** を意味するので、様々な用語が醸造に関連する用語になっています。
その他にも `Formula` は（より正確な意味での）レシピ・製法を意味します。これらの用語は、[公式ドキュメントの冒頭](https://docs.brew.sh/Formula-Cookbook#homebrew-terminology)で説明されています。
:::

:::note info
先程の `buildpath` や `prefix` などその他様々な関連ディレクトリがデフォルトで変数化され、用意されています。 その他のコマンドは、 [こちらの公式ドキュメント](https://docs.brew.sh/Formula-Cookbook#variables-for-directory-locations) を参考にしてください。
:::


## 3. manコマンドの設定ファイルを作成

manはgroffという文書整形を行うコマンドを利用してマニュアルを表示しています。しかし、Macにデフォルトでインストールされているgroffコマンドでは日本語翻訳ファイルが扱えないので、Homebrewで新しいバージョンをインストールするよう依存を張ります。

 `man-japanese.rb` のlicenseの下辺りに、 `depends_on "groff"` を追加するだけで、manjコマンドインストール時にgroffもインストールしてくれます。

```ruby
  license ""
  
  depends_on "groff"
```

manコマンドの設定ファイルは、通常 `/etc/man.conf` が利用されますが、 `-C file_path` と引数で渡してあげることで別の場所の設定ファイルを読み込むことが出来ます。

ここでは、デフォルトの `/etc/man.conf` を `buildpath` 下にコピーし、 `manj` 用に書き換えます。

`man-japanese.rb` のinstallメソッドの続きに、以下を追加します。

```ruby
    ...
    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メソッドの続きに、以下を追加します。


```ruby
    (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` に作成したファイルをそのまま渡します。

```bash:shell
$ brew install --build-from-source --debug Formula/man-japanese.rb
```

ここで `--debug` オプションを渡しておくと便利です。例えば、途中のインストールコマンドにtypo (`bash installman.s` hが抜けている) があった場合、エラーになった箇所で処理が一時停止し、 `irb` を立ち上げたり、 `shell` を起動することが可能です。特に `shell` を起動した場合、 `buildpath` の生成物が見られるのでデバッグが捗ります。

```bash
/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します。

```bash
sh0nk/homebrew-tap
.
└── Formula
    └── man-japanese.rb
```

すると、
```bash:shell
$ brew install sh0nk/tap/man-japanese
```

というコマンドでインストールできるようになります。 (`tap` の部分は先程の `homebrew-*` の `*` に対応)
ちなみにURLをフルで指定すれば、Githubじゃない場所からもインストールすることが可能です。

今回公開したレポジトリを参考に置いておきます。

https://github.com/sh0nk/homebrew-tap

:::note info
tapを利用せずにインストールさせたい場合は、 `homebrew-core` に対してpull requestを送って取り込んでもらうことで可能になります。その際は [いくつかのルール](https://docs.brew.sh/Acceptable-Formulae) を満たす必要があります。
:::

:::note info
こうやってみればわかるのですが、Homebrew Tapの仕組みを使うと、野良のレポジトリに入ったよくわからないものをインストールしてしまう危険性があります。その人のGithubレポジトリのFormulaを経由してマルウェアを仕込むようなことも可能になって来ますので、インストールする側の自衛として、Formulaを読めるようになっておくこともHomebrew Formulaを学ぶ一つのメリットと言えます。
:::

# Tips

Homebrewでは、Formulaで使えるruby文法のAPIが用意されています。

https://rubydoc.brew.sh/Formula

しかしAPIドキュメントだとさすがに細かすぎるかもしれません。実際のFormula作成時には、homebrew-coreに取り込まれているFormulaを参考にしながら書いていくと、推奨の書き方も理解できるので良いでしょう。

https://github.com/Homebrew/homebrew-core/tree/master/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` コマンドを活用してみる記事はこちらに書きましたので是非合わせてご賞味ください。

https://qiita.com/sh0nk/items/5d6449f92fdb3b23d221