Homebrewで自作Formulaを作るときの落とし穴

naoty/todo という CLI ツールを Homebrew で配布しようとしたときにハマったことを書く。

naoty/todo は Go で書かれており、コンパイル済みのバイナリを GitHub Releases にアップロードしてそこから配信したいと思っていた。ドキュメント等を調べると以下のように formula を書くことでインストールが完了するものと思っていた。

class Todo < Formula
  desc "A todo management tool just for myself"
  homepage "https://github.com/naoty/todo"
  url "https://github.com/naoty/todo/releases/download/0.2.0/todo.tar.gz"
  sha256 "be20e4069c0ae49998dfc00a010ca8f5d49d26193bd0c3e8611a4bf53cac469d"

  def install
    bin.install "todo"
  end
end

しかし、実際には Empty installation というエラーが発生してインストールができない現象に遭遇した。ドキュメントを調べてみるも、なぜこれが失敗するのか突き止めることはできなかった。そこで、エラーメッセージを頼りに Homebrew のソースコードを読むことにした。

まず、 Homebrew のソースコードは /usr/local/Homebrew/Library/Homebrew にある。そこで ag で Empty installation というエラーメッセージを検索してみると、以下のようなコードを見つけることができた。

if !formula.prefix.directory? || Keg.new(formula.prefix).empty_installation?
  raise "Empty installation"
end

ここからは pry を使ってブレークポイントを貼りながら進めようと思った。 Homebrew はシステムの Ruby を使っているようなので、 システムの rubygems で pry をインストールし調査を続けた。

binding.pry で調べたところ、 empty_installation?true を返しているようだった。このメソッドの中身は以下のようになっていた。

def empty_installation?
  Pathname.glob("#{path}/**/*") do |file|
    next if file.directory?
    basename = file.basename.to_s
    next if Metafiles.copy?(basename)
    next if %w[.DS_Store INSTALL_RECEIPT.json].include?(basename)
    return false
  end

  true
end

さらにここでイテレーションされている file を調べると formula でインストールした todoREADME 等のファイルが含まれていた。ここで何が原因か調べてみると、どうやら以下のように todo が README や LICENSE といったメタファイルのひとつとして扱われていて、ここで true が返っているようだった。

BASENAMES = Set.new %w[
  about authors changelog changes copying copyright history license licence
  news notes notice readme todo
].freeze

ということは、メタファイルではないものがひとつでも存在すれば true が返るということなので、以下のような formula を定義して適当なファイルを置くことで、この問題を回避することができた。

def install
  bin.install "todo"

  # Avoid "Empty installation" error which will be caused when the only
  # "todo" file is installed.
  bin.install "empty"
end

この問題は licensechangelog といった名前のパッケージを配布する場合でも起こる。ソースコードを読まないと原因が分からないので、同じ問題に直面した人は不運という感じがする。