HomebrewでLinuxとmacOSの両方に対応したFormulaを作って公開する

# はじめに

macOSとLinuxで利用できるパッケージ管理ツールの[Homebrew](https://brew.sh/)では、Formulaと呼ばれるパッケージの構成ファイルをRubyのコードで書くことで、新しいパッケージを追加することができます。
作成したFormulaを公開することでパッケージを世界中の人に使ってもらうことができます。

本稿ではFormulaの基本的な作成方法については省略して、macOSとLinuxの両方に対応したFormulaの作成・公開方法にフォーカスします。

# 単一プラットフォーム向けのFormulaがそのまま使えることもある

結論を一部先に述べる形になりますが、特にマルチプラットフォーム向けの対応をしなくても同じFormulaがそのまま使える場合もあります。

例:

- そもそもプラットフォーム依存のないソフトウェアである場合。例えば、ポータブルな形で書かれたシェルスクリプトとか
- ビルドツールがマルチプラットフォームに対応している場合。Go言語など
- ビルドスクリプトでプラットフォーム差異を吸収しているので、インストールコマンドがプラットフォームによって変わらない場合

Linuxbrewの場合、デフォルトのFormulaリポジトリとしては[linuxbrew-core](https://github.com/Homebrew/linuxbrew-core)を参照していますが、こちらには1日1回程度のペースで本家（？）である[homebrew-core](https://github.com/Homebrew/homebrew-core)の差分がマージされています。[^1]

[^1]: 参考: https://github.com/Homebrew/linuxbrew-core/blob/master/CONTRIBUTING.md

なので、元々プラットフォーム依存のないソフトウェア・Formulaであれば、作者が特に意識して対応していなくても、両方のプラットフォームで利用できる可能性はなくはないです。
（実際、私はLinuxbrewを使っていますが、公式にはLinux対応と書かれていないパッケージが使えたことがあります。）

# 1. Formulaを作る
## 1.1. 雛形の作成

普通にFormulaを作る場合と変わりません。
`brew create` コマンドで作成できます。

```sh
# 構文
brew create [Options] URL

# 例
## カスタムのTAPを指定し、Go言語プロジェクト向けの雛形を作る
brew create --go --tap progrhyme/taps -v https://github.com/progrhyme/shelp/archive/v0.5.0.tar.gz
```

この `brew create` コマンドには様々なオプションがあり、各種ビルド方式に対応した雛形があるので、一度 `brew create --help` を見てみると良いでしょう。

## 1.2. LinuxとmacOSで必要な分岐処理を書く

LinuxとmacOSで処理を分けるためには、以下の4つのメソッドが利用できます:

- [Formula.on_linux](https://rubydoc.brew.sh/Formula.html#on_linux-class_method), [Formula.on_macos](https://rubydoc.brew.sh/Formula.html#on_macos-class_method)
- [OS.linux?](https://rubydoc.brew.sh/OS.html#linux%3F-class_method),  [OS.mac?](https://rubydoc.brew.sh/OS.html#mac%3F-class_method)

利用イメージはこんな感じです:

```Ruby
class Foo < Formula
  on_macos do
    # macOS向けのレシピ
  end
  on_linux do
    # Linux向けのレシピ
  end

  def install
    if OS.mac?
      # macOS向けのインストール処理
    elsif OS.linux?
      # Linux向けのインストール処理
    end
  end
end
```

[Homebrew FormulaのRuby API](https://rubydoc.brew.sh/)には、これ以外にもプラットフォーム固有の処理を行う上で有用そうなモジュールやメソッドがあるので、調べてみるとよさそうです。

### サンプル① linuxbrew-core/rust.rb

RustコンパイラやcargoのFormulaである
[linuxbrew-core/rust.rb](https://github.com/Homebrew/linuxbrew-core/blob/master/Formula/rust.rb)で、上に挙げたメソッドがいくつか使われているので、抜粋して見てみます:

```Ruby:Formula/rust.rb（抜粋）
class Rust < Formula
  desc "Safe, concurrent, practical language"
  homepage "https://www.rust-lang.org/"

  stable do
    url "https://static.rust-lang.org/dist/rustc-1.44.0-src.tar.gz"
    sha256 "bf2df62317e533e84167c5bc7d4351a99fdab1f9cd6e6ba09f51996ad8561100"

    resource "cargo" do
      url "https://github.com/rust-lang/cargo.git",
          :tag      => "0.45.0",
          :revision => "05d080faa4f2bc1e389ea7c4fd8f30ed2b733a7f"
    end
  end

  bottle do
    : # 略
  end

  depends_on "cmake" => :build
  : # 略

  on_linux do
    depends_on "binutils"
  end

  uses_from_macos "curl"
  uses_from_macos "zlib"

  resource "cargobootstrap" do
    if OS.mac?
      url "https://static.rust-lang.org/dist/2020-05-07/cargo-0.44.0-x86_64-apple-darwin.tar.gz"
      sha256 "1071c520204a9e8fe4dd0de66a07a083f06abba16ac88f1df72231328a6395e6"
    end
    unless OS.mac?
      url "https://static.rust-lang.org/dist/2020-05-07/cargo-0.44.0-x86_64-unknown-linux-gnu.tar.gz"
      sha256 "e4c8533670a64a85ee1a367e4bb9d63a2d6b3e9949e116ec956cec15df9a67bd"
    end
  end

  def install
    ENV.prepend_path "PATH", Formula["python@3.8"].opt_libexec/"bin"
    ENV["MACOSX_DEPLOYMENT_TARGET"] = MacOS.version if OS.mac?
    ENV["OPENSSL_DIR"] = Formula["openssl@1.1"].opt_prefix
    ENV["SDKROOT"] = MacOS.sdk_path if OS.mac?

    args = ["--prefix=#{prefix}"]
    : # 略
    system "./configure", *args
    system "make"
    system "make", "install"

    : # 略
  end
end
```

ここでは上に挙げた4つのメソッド以外に、以下のメソッド、モジュールが使われています:

- [Formula.uses_from_macos](https://rubydoc.brew.sh/Formula.html#uses_from_macos-class_method) ... ドキュメントによれば、macOSでは何もせず（システムライブラリを使う）、Linuxでは `depends_on` と同じ動きをするそうです
- module MacOS ... これは[module OS::Mac](https://rubydoc.brew.sh/OS/Mac.html)の別名として定義されていました。[^2] macOSマシンの各種情報を取得するメソッドが用意されています

[^2]: https://github.com/Homebrew/brew/blob/2.4.0/Library/Homebrew/os/mac.rb#L15

### サンプル② progrhyme/taps/shelp.rb

自前でTapを用意する場合のサンプルとして、拙作のFormulaを紹介します:

- [homebrew-taps/shelp.rb](https://github.com/progrhyme/homebrew-taps/blob/master/Formula/shelp.rb)

```Ruby:Formula/shelp.rb
class Shelp < Formula
  Version = '0.5.3'
  version Version

  if OS.mac?
    Binary = "shelp_#{Version}_darwin_x86_64"
    sha256 "dffb6ccb90395d2e9d90b3dc7f80736a30d6b5be42c99e99b6b3df87e965e30c"
  elsif OS.linux?
    Binary = "shelp_#{Version}_linux_x86_64"
    sha256 "7f35438588860e8f372ce4b3dbed96abc848ec35cb4a7826df49f033d595277d"
  end

  url "https://github.com/progrhyme/shelp/releases/download/v#{Version}/#{Binary}"
  desc 'Git-based package manager for shell scripts written in Go'
  homepage 'https://github.com/progrhyme/shelp'
  head 'https://github.com/progrhyme/shelp.git'

  def install
    bin.install Binary => 'shelp'
  end

  test do
    system "#{bin}/shelp", '--version'
  end
end
```

Go言語でビルドする形でFormulaを書けば特に分岐を作る必要もありませんが、わざわざビルドしてもらうためにGoをインストールしてもらうのも気が引けるので、GitHub Releaseにアップロード済みのバイナリを配布する形にしています。

野良Tapなのでbottleで配布できないので、このようにOSを判定してダウンロードURLを分けています。

# 2. Formulaを公開する

基本的に、次のいずれかの方法を取ることになると思います。

- 公式の[homebrew-core](https://github.com/Homebrew/homebrew-core)と[linuxbrew-core](https://github.com/Homebrew/linuxbrew-core)に取り込んでもらう
- 自前でTapを用意して配布する

これらのやり方については既に多くの解説記事が書かれており、マルチプラットフォーム対応する場合もそれほど手続きに差異はありません。
ので、共通する部分はさらっと行きます。

## 公式リポジトリに取り込んでもらう

通常と同様に、[homebrew-core](https://github.com/Homebrew/homebrew-core)にプルリクエストを送って取り込んでもらう形になります。

先にも述べたように、[linuxbrew-core](https://github.com/Homebrew/linuxbrew-core)には1日1回程度のペースで最新のhomebrew-coreがマージされているようなので、基本的にこちらにプルリクエストを送る必要はありません。

公式のガイドは下になります:

- [How To Open a Homebrew Pull Request — Homebrew Documentation](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request)

## 自前でTapを用意して配布する

Tapとは「Third-Party Repositories」のことで、非公式のFormulaリポジトリをbrewの仕組みで管理できるようにするためのものです。[^3]
`brew tap` してインストールするアレです。

[^3]: https://github.com/Homebrew/brew/blob/master/docs/Taps.md

公式のhomebrew-coreにプルリクエストを送るのがハードルが高い場合、こちらのやり方を採るとよいです。

こちらについても、やり方については既に巷に多くの記事が書かれているので、詳細は割愛します。

私の場合、上にも挙げた[progrhyme/homebrew-taps](https://github.com/progrhyme/homebrew-taps)というリポジトリを作ったので、基本的にこちらにまとめていこうと考えています。（参考記事のやり方を参考にしました。）

# まとめ

本稿では、HomebrewでLinuxとmacOSの両方に対応したFormulaを作って、公開する方法を紹介しました。

# 参考

- [RustのソフトウェアをHomebrewのパッケージにする - Qiita](https://qiita.com/dalance/items/b07bee6cadfd4dd19756#macos%E7%94%A8bottle%E3%81%AE%E7%94%9F%E6%88%90) ... この当時はlinuxbrew-coreに取り込んでもらった後、bottleを生成してもらうために手動でイシューを立てる必要があったそうです
- [Homebrewで自作ツールを簡単にインストール可能にする | おそらくはそれさえも平凡な日々](https://songmu.jp/riji/entry/2019-02-22-maltmill.html)