Homebrewに新しいFormulaの追加リクエストをしてみた

ツール

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。GW明け一発目でまずはブログのお仕事。

先日、お送りしたCloudFormationテンプレートをYAMLで書いてみる【remarshal】というエントリ内で

remarshalはMacを使っていれば、homebrewで一発インストールが可能です。

と書きました。一言さらりと説明しましたが、実はこれ、私がremarshalを見つけた時点ではhomebrewで入るものではなく、Goをインストールして自分でビルドする必要がありました。でもhomebrewで入れて使いたいですよね。というわけで…。

作戦1: 作者にHomebrewによるリリースを頼んでみる

私「これ、Homebrewでリリースする気ない?」
作者様「俺、MacOSX使って無いんだよねー。YouやっちゃいなYo!」

なるほど…。

参考: https://github.com/dbohdan/remarshal/issues/3

作戦2: 野良Homebrewリポジトリにリリースする

結論からいうと、この作戦は実施しませんでした。が、こういう方法もあるということでご紹介します。

homebrewは、デフォルトではGitHub上のHomebrew公式リポジトリに登録してあるパッケージを利用できます。が、brew tapというコマンドを用いることによって、野良リポジトリを利用できます。

野良リポジトリは、YOUR_USERNAME/repoという形式で名前がついています。自分のGitHubアカウント(例えばここではYOUR_USERNAME=fooとします)で、homebrew-barというリポジトリを作ると、foo/barというリポジトリ名が付きます。GitHubリポジトリ名の接頭辞homebrew-は必須です。

参考: https://github.com/Homebrew/homebrew/blob/master/share/doc/homebrew/brew-tap.md

こうして作ったリポジトリのmasterブランチ直下に、例えばfoobar.rbというFormula(Homebrew用のインストールスクリプト)を配置(ファイル名のbasename部分が、Formula名になります)しておけば、下記のようなコマンドでfoobarのインストールができます。

$ brew tap foo/bar       # 野良リポジトリ「foo/bar」を登録
$ brew install foobar    # Formula名「foobar」をインストール

FormulaはRubyベースのDSLで記述しますが、書き方はご紹介しきれないので、詳細はExample formulaFormula Cookbookを御覧ください。また、公式リポジトリ上のFormulaも生きた実例ですので、大変参考になると思います。

作戦3: 本家HomebrewにformulaのPull Requestを入れる

そうではなく、やはりtap無しで普通に公式リポジトリからパッケージインストールをしたいですね。となると、公式リポジトリにFormulaを追加するためのPull Requestを行うことになります。

参考: How To Open a Homebrew Pull Request (and get it merged)

細かくは上記の公式ドキュメントを読んでいただくのが最も確実ですが、以下に概略を。

本家リポジトリをForkし、ローカルリポジトリにリモート登録する

まず、cd $(brew --repository)コマンドで、ローカルマシン上のHomebrewがインストールされているディレクトリ(デフォルトでは/usr/local)に移動します。参考までに、このディレクトリでgit remote -vすると、このディレクトリ自体がhttps://github.com/Homebrew/homebrewをcloneしたリポジトリであることがわかると思います。

次に、GitHub上でHomebrew/homebrewGitリポジトリを、自分のアカウントにForkします。Forkは左記リンクから。その結果、自分のGitアカウント上にYOUR_USERNAME/homebrewというGitリポジトリが出来上がります。

この自分のGitリポジトリを、ローカル上にcloneしてあるリポジトリ/usr/localに対するリモートリポジトリとして登録します。

$ git remote add YOUR_USERNAME https://github.com/YOUR_USERNAME/homebrew.git

マニュアルには上記のように書かれていますが、SSH及び鍵認証を行う場合は、下記のようにしても構いません。

$ git remote add YOUR_USERNAME ssh://git@github.com/YOUR_USERNAME/homebrew.git

最新版からブランチを生やしてcommit, push, pull-request

git checkout masterによりmasterブランチに居ることを確実にし、さらにbrew updateコマンド(この裏側でgit pullが呼ばれる)で最新状態を取得します。

ここからgit checkout -b YOUR_BRANCH_NAME origin/masterコマンドで新しいブランチ(PR用)を作ります。今回の場合、私は安直にYOUR_BRANCH_NAME=add_remarshalとしました。

このブランチでLibrary/Formula/remarshal.rbにFormulaを記述してコミットします。

Formulaを作りながら、下記コマンドでエラーが出ないように頑張ります。これでエラーが出る状態だと、そもそもプルリクが受け入れられる可能性はほぼ無いはずですので、頑張ってください。

$ brew audit ANY_CHANGED_FORMULA
$ brew tests
$ brew install ANY_CHANGED_FORMULA && brew test ANY_CHANGED_FORMULA

当初私が書いたFormulaは下記のようなものでした。

require "formula"

class Remarshal < Formula
  homepage "https://gowalker.org/github.com/dbohdan/remarshal"
  url 'https://github.com/dbohdan/remarshal.git', :tag => "v0.3.0", :revision => 'bb9ac467ca297c55587a798531886e6159acbcbd'

  head "https://github.com/dbohdan/remarshal.git", :branch => 'master'

  depends_on "go" => :build
  depends_on :hg => :build

  def install
    ENV["GOPATH"] = buildpath

    # Install Go dependencies
    system "go", "get", "github.com/BurntSushi/toml"
    system "go", "get", "gopkg.in/yaml.v2"

    # Build and install
    system "go", "build", "-o", "remarshal"
    bin.install 'remarshal'

     Array["toml", "yaml", "json"].each do |informat|
      Array["toml", "yaml", "json"].each do |outformat|
        bin.install_symlink "remarshal" => "#{informat}2#{outformat}"
      end
    end
  end
end

ビルドディレクトリでgoコマンドをsystemで呼んでビルドして、成果物をインストール。最後にyaml2json等のシンボリックリンクを作る、というスクリプトです。

後で思い知りますが、前述の自動チェックで問題は指摘されないものの、上記はまだ色々不完全で、ダメな例です。

コミットメッセージはremarshal: add remarshal 0.3.0のようにシンプルにすると良いようです。複数コミットして作った場合は、squashして1コミットにまとめましょう。

コミットしたら、git push --set-upstream YOUR_USERNAME YOUR_BRANCH_NAMEコマンドでリモート(Forkして後から追加したもの)にpushしてから、https://github.com/Homebrew/homebrewからPull Requestを作成します。PRのタイトルは他に倣って "FORMULA_NAME NEW_VERSION" 形式としました。

メンテナからレビューを受ける

さて、私が作ったPRはこれ。 https://github.com/Homebrew/homebrew/pull/39104

PRを送って間もなく、数々のレビューが入ります。

  • require "formula"要らないよ
  • (シングルクオートは使わずに)全てダブルクオートで書こう
  • テスト書いて
  • (シンボリックリンク作る所は)コレでいいんじゃない?
    ["toml", "yaml", "json"].combination(2).each do | informat, outformat|
  • Goのビルドはgo_resourceを使おう。aptly.rbを参考にしてね。

指摘を受けて修正コミット。この場合やはり複数コミットになってしまうので、git rebase --interactive origin/masterコマンドで1つのコミットにsquashします。

その結果、force pushが必要になりますが、それは許容されます。git push --forceにて。

...
  test do
    require 'open3'
    Open3.popen3("#{bin}/remarshal", '-if=json', '-of=yaml') do |stdin, stdout, _|
      stdin.write('{ "foo.bar": "baz", "qux": 1 }')
      stdin.close
      assert_equal "foo.bar: baz\nqux: 1\n\n", stdout.read
    end
...
  • open3じゃなくてpipe_output使うといいよ
  • homepageは https://github.com/dbohdan/remarshal である必要があるね

という調子で、何度かメンテナによるレビューのやり取りを続けて出来たのがコレ

require "language/go"

class Remarshal < Formula
  homepage "https://github.com/dbohdan/remarshal"
  url "https://github.com/dbohdan/remarshal.git", :tag => "v0.3.0", :revision => "bb9ac467ca297c55587a798531886e6159acbcbd"

  head "https://github.com/dbohdan/remarshal.git", :branch => "master"

  depends_on "go" => :build
  depends_on :hg => :build

  go_resource "github.com/BurntSushi/toml" do
    url "https://github.com/BurntSushi/toml.git", :revision => "443a628bc233f634a75bcbdd71fe5350789f1afa"
  end

  go_resource "gopkg.in/yaml.v2" do
    url "https://gopkg.in/yaml.v2.git", :revision => "49c95bdc21843256fb6c4e0d370a05f24a0bf213"
  end

  def install
    ENV["GOPATH"] = buildpath
    mkdir_p buildpath/"src/github.com/dbohdan/"
    ln_sf buildpath, buildpath/"src/github.com/dbohdan/remarshal"
    Language::Go.stage_deps resources, buildpath/"src"

    # Build and install
    system "go", "build", "-o", "remarshal"
    bin.install "remarshal"

    ["toml", "yaml", "json"].permutation(2).each do |informat, outformat|
      bin.install_symlink "remarshal" => "#{informat}2#{outformat}"
    end
  end

  test do
    json = <<-EOS.undent.chomp
      {
        "foo.bar": "baz",
        "qux": 1
      }

    EOS
    yaml = <<-EOS.undent.chomp
      foo.bar: baz
      qux: 1


    EOS
    toml = <<-EOS.undent.chomp
      "foo.bar" = "baz"
      qux = 1


    EOS
    assert_equal yaml, pipe_output("#{bin}/remarshal -if=json -of=yaml", json)
    assert_equal yaml, pipe_output("#{bin}/json2yaml", json)
    assert_equal toml, pipe_output("#{bin}/remarshal -if=yaml -of=toml", yaml)
    assert_equal toml, pipe_output("#{bin}/yaml2toml", yaml)
    assert_equal json, pipe_output("#{bin}/remarshal -if=toml -of=json", toml)
    assert_equal json, pipe_output("#{bin}/toml2json", toml)
  end
end

祝・マージ

という感じで、最後に「PRありがとう! Homebrewはあなたのようなコミュニティメンバーからのコントリビュートによって成り立っています。あなたのサポートに感謝します。」というお言葉を頂きつつ、マージしてもらえました。

まとめ

初めてのHomebrewへのPRということで奮闘記風にまとめてみました。Formula記述に慣れていなくても、上記の通り、メンテナの方が懇切丁寧に色々教えてくれます。大変助かりました。

みなさんも、Mac上で使っているツールがhomebrewによるインストールに対応していなかったら、PRを出してみることをお勧めします!

AWS Cloud Roadshow 2017 福岡