Swift Package Manager(SwiftPM)で作ったコマンドラインツールをHomebrewに登録する方法

  • 35
    いいね
  • 3
    コメント

mono0926/LicensePlist という、Carthage・CocoaPodsおよび手動指定ライブラリから、iOS設定アプリに載せられるplist形式のライセンスファイル群を生成するコマンドラインツールを作りまして、それをHomebrewに登録したのでやり方を共有します。

結果的にはまあまあ簡単ですが、Homebrew登録自体が初めてだったので、多少手こずりました。

前提

  • Swift Package Manager(SwiftPM)で作ったコマンドラインツールのHomebrew登録
  • 本家Homebrewリポジトリへの登録ではなく、brew tap方式
  • Bottlesという、バイナリ配布対応はスキップしたため、インストール先で都度ビルドが必要

対応手順

Makefile を作成

本体のコマンドラインツールリポジトリで Makefile を用意します。SwiftPM ではHomebrew対応せずとも簡単なMakefileを用意するのがわりと一般的なので、Homebrew対応前からすでに作成済みだったりもすると思います。

PREFIX?=/usr/local

build:
    swift build -c release -Xswiftc -static-stdlib

install: build
    mkdir -p "$(PREFIX)/bin"
    cp -f ".build/release/LicensePlist" "$(PREFIX)/bin/license-plist"

installにて、次に作るFormulaファイルから渡す$(PREFIX)を使うように設定するのが肝です。そのディレクトリを生成してビルドされたバイナリファイルをコピーしています。PREFIX?=/usr/localを代入しているので、普通にmake installした場合は、/usr/local/bin配下にコピーされます。

Homebrew対応の本筋とは外れますが、swift build -c release -Xswiftc -static-stdlib-Xswiftc -static-stdlib オプションも大事です。標準ライブラリおよび依存ライブラリとstatic linkしてくれるので、単一実行バイナリが生成できます。
以下の記事で紹介されていました。このオプション指定以外にも大いに参考にさせていただきました🙇
Building a command line tool using the Swift Package Manager

参考に、こちらがLicensePlistのMakefileです

出来たら、Pushしてタグを打っておきましょう。以下では1.3.5タグを打ったとして説明していきます。

Formulaファイル作成

https://github.com/mono0926/LicensePlist がコマンドラインツール本体ですが、それに対応する
https://github.com/mono0926/homebrew-license-plist というbrew tap用のリポジトリを作成します本体のコマンドラインツールリポジトリがキャメルケースだったので、ちょっと揺れが出てしまっています…)。
今回は名前を合わせましたが、合わせなくても良いです。mono0926/tapsなどのリポジトリ名の方が今後他の時にも流用出来て良かったかも、と今思っています…。

まずは以下を実行してひな形を生成します。

$ brew create https://github.com/mono0926/LicensePlist/archive/1.3.5.tar.gz

https://github.com/mono0926/LicensePlist/archive/1.3.5.tar.gz 部分は、リポジトリのURL/archive/タグ.tar.gz という構成になっています。これは、タグを打つと、その時点のスナップショットとそのURLをGitHubが自動生成してくれるようになっています(Releasesに載っているダウンロードURLと同じです)。

Editing /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/licenseplist.rb というメッセージとともにひな形の編集画面が開きます。

ひな形はこうなっています。

# Documentation: http://docs.brew.sh/Formula-Cookbook.html
#                http://www.rubydoc.info/github/Homebrew/brew/master/Formula
# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!

class LicensePlist < Formula
  desc "A license list generator of all your dependencies for iOS applications"
  homepage ""
  url "https://github.com/mono0926/LicensePlist/archive/1.3.5.tar.gz"
  sha256 "bae83138b45e062cf1cfa8ee627d7cf0912147e56a9bd93f643b63125d6dd182"

  # depends_on "cmake" => :build

  def install
    # ENV.deparallelize  # if your formula fails when building in parallel

    # Remove unrecognized options if warned by configure
    system "./configure", "--disable-debug",
                          "--disable-dependency-tracking",
                          "--disable-silent-rules",
                          "--prefix=#{prefix}"
    # system "cmake", ".", *std_cmake_args
    system "make", "install" # if this fails, try separate make/make install steps
  end

  test do
    # `test do` will create, run in and delete a temporary directory.
    #
    # This test will fail and we won't accept that! It's enough to just replace
    # "false" with the main program this formula installs, but it'd be nice if you
    # were more thorough. Run the test with `brew test LicensePlist`. 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

PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!と書かれた通り消しましょう(brew tap形式なので残ってても問題無いですが)。

class LicensePlist < Formula
  desc "A license list generator of all your dependencies for iOS applications"
  homepage ""
  url "https://github.com/mono0926/LicensePlist/archive/1.3.5.tar.gz"
  sha256 "bae83138b45e062cf1cfa8ee627d7cf0912147e56a9bd93f643b63125d6dd182"

  def install
    system "./configure", "--disable-debug",
                          "--disable-dependency-tracking",
                          "--disable-silent-rules",
                          "--prefix=#{prefix}"
    system "make", "install"
  end

  test do
    system "false"
  end
end

そして、SwiftPMのコマンドラインツールをうまくビルドできるように編集して、以下が完成系です。

class LicensePlist < Formula
  desc "A license list generator of all your dependencies for iOS applications"
  homepage ""
  url "https://github.com/mono0926/LicensePlist/archive/1.3.5.tar.gz"
  sha256 "bae83138b45e062cf1cfa8ee627d7cf0912147e56a9bd93f643b63125d6dd182"

  def install
    system "make", "install", "PREFIX=#{prefix}"
  end

  depends_on :xcode => ["8.3", :build]
end

先ほどのMakefileで参照できるように、installメソッドで, "PREFIX=#{prefix}"にてPREFIX環境変数を渡すのが肝です。ちなみに、以下のようなログが出て、/usr/local/Cellar/license-plist/1.3.5がセットされていることが分かります。

$ make install PREFIX=/usr/local/Cellar/license-plist/1.3.5

testは使わずで良いと思ったのでカットしましたが、必要に応じて記述してください。

depends_onは無くとも動きますが、 @ikesyo さんに指摘していただき、付け足しました。あった方が親切ですね。

うまくインストールできるか試します(ローカルではFormula以下にあるので普通にインストールできます)。

$ brew install license-plist

また。ls -lで詳細情報を見ると、先ほどのPREFIX配下に実態があって、シンボリックリンクが張られていることが分かります。

$ ls -l /usr/local/bin/license-plist
$ lrwxr-xr-x  1 mono  admin  47 May 20 20:51 /usr/local/bin/license-plist -> ../Cellar/license-plist/1.3.5/bin/license-plist

うまくいったら、以下のようにbrew tap用のリポジトリに移動してPushしましょう。

$ mv /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/licenseplist.rb "homebrew-license-plist をチェックアウトした場所"

こちらがhomebrew-license-plistのFormulaです

確認

以下のコマンドを実行して、インストール成功すればOKです。エラー出たら、解決がんばりましょう💪

$ brew install mono0926/license-plist/license-plist

オーナー名/brew tap用に作ったリポジトリからhomebrew-の接頭辞を省いた名前/Formula名という構成です。
ちなみに、以下の短縮形です。

$ brew tap mono0926/license-plist
$ brew install license-plist

アップデート作業

本体リポジトリでのリリースの度に次のような操作が必要です。

  1. 本体リポジトリでタグを打つ
  2. brew tap用のリポジトリでurlに記述してあるタグ部分を書き換え、さらに sha256 も更新してPush
  3. brew upgrade license-plistを実行して確認

さらに sha256 も更新

これが地味に面倒なのですが、愚直にダウンロードしてsha256計算するしか無いのですかね?🤔良いやり方ご存じでしたら教えてください🙇

とりあえず以下のスクリプトを作ったので、./copy_sha256.sh 1.3.5を実行するとダウンロード・sha256の計算に加えて、さらにクリップボードにコピーまでしてくれて面倒さは解消しました(スクリプト用意しなくて良い楽な方法があれば不要になってしまいますが…)。

if [ $# -eq 0 ]; then
    echo "A tag argument is needed!(ex: ./copy_sha256 1.2.3)"
    exit 1
fi
tag=$1
echo "Tag: '${tag}'"
filename="${tag}.tar.gz"
echo "Filename: '${filename}'"
curl -LOk "https://github.com/mono0926/LicensePlist/archive/${filename}"
result=$(shasum -a 256 $filename)
echo "Result: '${result}'"
sha256=$(echo ${result} | cut -d ' ' -f 1)
echo $sha256 | tr -d '\n' | pbcopy
echo "sha256('${sha256}') was copied to your clipboard🎉"
rm $filename

今後の更新作業がスムーズになるようにこれもリポジトリに含めましたアップデート更新作業全体を自動化したいなどとも思っています

その他

Bottles対応を省いた理由

単純にBottlesやその他解説記事の通りにやってみたら、各プラットフォーム向けにバイナリファイル生成したり、さらにそれを更新していくのが手間(手動にしても自動スクリプト書くにしても)だと思ったからです。

Bottles対応を省くと以下のデメリットはあります。

  • インストール・アップデート時に、ビルド時間が少しかかる
    • Bottles対応しておけばバイナリファイルダウンロード時間だけで済む
  • 過去バージョンのバイナリをインストールできず、常に最新版のタグになってしまう

過去の版欲しい場合(デグレなどで最新版で動かなくなってしまった時など)は、一応Releasesからダウンロードする方法も提供していますし、あるいは過去のタグでビルドしてもらっても一応何とかなるので、まあ良いかなと今のところ思っています。

参考リンク

LicensePlistのロゴ

ロゴ作っちゃいました( ´・‿・`)

LicensePlist.png

設定アプリに載るライセンスファイル一覧生成なので、設定アプリアイコンっぽい感じにして、さらにCopyright @ monoでライセンスっぽさを加えました( ´・‿・`)