8
Help us understand the problem. What are the problem?
Organization

はじめての自作Homebrew Formula

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つです。

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

1. テンプレートの作成

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

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

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

すると、以下のようにFormulaのテンプレートが作成され、エディタが開きます。Formulaファイルは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 メソッド前後をこのように書き換えます。

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 ブロックに指定します。

その後、

    resource("installman").stage do
      cp("installman.sh", buildpath)
    end

でこのファイルをダウンロードし、 cp メソッドで buildpath にコピーします。

ここで、buildpathbrew install 実行時に作られるテンポラリディレクトリです。デフォルトだとインストール対象である man-pages-ja-20220615.tar.gz が展開された状態になります。

この installman.sh はインストール先パスを CELLAR_PATH という変数で受け取るので、これを環境変数にセットし、 mkdir_p でインストール先パスを作成、 bashinstallman.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 は(より正確な意味での)レシピ・製法を意味します。これらの用語は、公式ドキュメントの冒頭で説明されています。

先程の buildpathprefix などその他様々な関連ディレクトリがデフォルトで変数化され、用意されています。 その他のコマンドは、 こちらの公式ドキュメント を参考にしてください。

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.confbuildpath 下にコピーし、 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 に作成したファイルをそのまま渡します。

shell
$ 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

すると、

shell
$ 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を参考にしながら書いていくと、推奨の書き方も理解できるので良いでしょう。

参考文献

おまけ

今回作った manj コマンドを活用してみる記事はこちらに書きましたので是非合わせてご賞味ください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
8
Help us understand the problem. What are the problem?