rcmdnk's blog

How to Brew: Everything You Need to Know to Brew Great Beer Every Time

Pythonで作ったCLIツールは PyPI を通して配布することが出来ますが 必要なPythonのバージョンがあったり、依存パッケージを色々インストールしないといけないとなると pipでグローバルな環境に入れるのもちょっと微妙。

仮想環境を作ってそこで管理すれば他のライブラリ等を気にせず使えますが、 Homebrewを使うとその辺が簡単に出来ます。

HomebrewでのPythonツール用のFormulaの作成

以下にPythonのFormulaに関するドキュメントがあります。

Python for Formula Authors — Homebrew Documentation

ここだと色々と思想的なものが書かれているのとちょっと古い?方法が書かれていますが、

以下ではPyPIで配布されているパッケージのFormulaを作る際に実際にやりやすい手順を紹介。

brew create

まず、テンプレートを作るため、brew createコマンドを使います。

ただ、これ今ちょっと使いづらくなっています。 デフォルトではHomebrew-coreのTapの中に作る設定になっていますが 最近API経由のインストールがHomebrewのデフォルトになっているので そのTapが存在しないため

1
Error: No available tap .

といったエラーが出てしまいます。

なので適当なTapを用意して上げる必要があります。 homebrew-core を入れるでも良いのですが重いので適当な別のものを入れたほうが楽です。

もし適当なものがなければhomebrew-testtapという名前の空レポジトリをGitHubに作って

1
$ brew tap <user>/testtap

とかで入れればOK 1

rcmdnk/homebrew-testtap が空Tapなので、これを使っても良いです。

ローカルなtapを持ったらbrew createを実行します。

1
Usage: brew create [options] URL

brew create--python, --ruby, --goなど各言語の独自環境やビルド用の templateを作るためのオプションが用意されているのでこれを使います。

python用FormulaではURLにはPyPIでのSourceの圧縮ファイルを指定します。

以下では cocoro というCLIツールで作ってみます。

PyPIのcocoroのファイルをダウンロード のページに有る最新のtar.gzのファイルのURLを取得。

これを使って以下のようなコマンドを実行。

--set-nameオプションはつけない場合はデフォルトの値がcocoro-0.1.4とバージョンの付いた名前になって、 これを変更するかどうか聞かれるプロンプトが出ます。

バージョンは外さないと行けないので最初から--set-nameで指定しています。

--set-licenseでのライセンス指定などは後からエディタで変更してもOK。

1
$ brew create --python --set-name cocoro --set-license Apache-2.0 --tap rcmdnk/testtap https://files.pythonhosted.org/packages/33/3a/7ada2648654931f7b01885eb14eb0d7988115247146ece39fc6b1cf0ff6a/cocoro-0.1.4.tar.gz

これで以下のようなファイルができ、それをエディタで開いた状態になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# Documentation: https://docs.brew.sh/Formula-Cookbook
#                https://rubydoc.brew.sh/Formula
# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!
class Cocoro < Formula
  include Language::Python::Virtualenv

  desc ""
  homepage ""
  url "https://files.pythonhosted.org/packages/33/3a/7ada2648654931f7b01885eb14eb0d7988115247146ece39fc6b1cf0ff6a/cocoro-0.1.4.tar.gz"
  sha256 "170404bfe64c80e6742689225995a23708855f445dd7b65ff690ad7f2278d417"
  license "Apache-2.0"

  depends_on "python"

  resource "certifi" do
    url "https://files.pythonhosted.org/packages/37/f7/2b1b0ec44fdc30a3d31dfebe52226be9ddc40cd6c0f34ffc8923ba423b69/certifi-2022.12.7.tar.gz"
    sha256 "35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"
  end

  resource "charset-normalizer" do
    url "https://files.pythonhosted.org/packages/ff/d7/8d757f8bd45be079d76309248845a04f09619a7b17d6dfc8c9ff6433cac2/charset-normalizer-3.1.0.tar.gz"
    sha256 "34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"
  end

  resource "fire" do
    url "https://files.pythonhosted.org/packages/11/07/a119a1aa04d37bc819940d95ed7e135a7dcca1c098123a3764a6dcace9e7/fire-0.4.0.tar.gz"
    sha256 "c5e2b8763699d1142393a46d0e3e790c5eb2f0706082df8f647878842c216a62"
  end

  resource "idna" do
    url "https://files.pythonhosted.org/packages/8b/e1/43beb3d38dba6cb420cefa297822eac205a277ab43e5ba5d5c46faf96438/idna-3.4.tar.gz"
    sha256 "814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"
  end

  resource "PyYAML" do
    url "https://files.pythonhosted.org/packages/a0/a4/d63f2d7597e1a4b55aa3b4d6c5b029991d3b824b5bd331af8d4ab1ed687d/PyYAML-5.4.1.tar.gz"
    sha256 "607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"
  end

  resource "requests" do
    url "https://files.pythonhosted.org/packages/9d/ee/391076f5937f0a8cdf5e53b701ffc91753e87b07d66bae4a09aa671897bf/requests-2.28.2.tar.gz"
    sha256 "98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"
  end

  resource "six" do
    url "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz"
    sha256 "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"
  end

  resource "termcolor" do
    url "https://files.pythonhosted.org/packages/e5/4e/b2a54a21092ad2d5d70b0140e4080811bee06a39cc8481651579fe865c89/termcolor-2.2.0.tar.gz"
    sha256 "dfc8ac3f350788f23b2947b3e6cfa5a53b630b612e6cd8965a015a776020b99a"
  end

  resource "urllib3" do
    url "https://files.pythonhosted.org/packages/21/79/6372d8c0d0641b4072889f3ff84f279b738cd8595b64c8e0496d4e848122/urllib3-1.26.15.tar.gz"
    sha256 "8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"
  end

  def install
    # ENV.deparallelize  # if your formula fails when building in parallel
    virtualenv_install_with_resources
  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 cocoro`. 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

ファイルはtapの中の/opt/homebrew/Library/Taps/rcmdnk/homebrew-testtap/cocoro.rb (環境によって違う、$(brew --repo rcmdnk/testtap)/cocoro.rb)にあります。

このファイルは$ brew edit cocoroとすることでもエディタで開くことが出来ます。

URLで指定したものがurlに入り、そのsha256の値は自動的に取得されています。

依存するHomebrewのパッケージとしてはpythonがあります。 もし(Homebrewで入れられる)特定のPythonのバージョンを使いたい場合は [email protected]などに変更します。

Homebrew-coreに入れる場合など、testをきちんと定義したい場合はpythonというエイリアスだと

1
2
$ brew test cocoro
Error: No available formula with the name "python".

といったエラーになってしまうので[email protected]などきちんとバージョン含めたパッケージ名にする必要があります。

pyproject.tomlsetup.pyに書かれた依存関係のあるパッケージに関してはすべて resourceとして追加されています。

きちんと定義されたパッケージであればもうこれでOKで、後は descに適当な紹介文を、homepageにPyPIのパッケージのページかGitHubのページ、 もしくはちゃんとしたHomepageがあればそれを記入しておけばとりあえず完成です。

もし依存関係の記述が不十分で他に必要なパッケージがある場合には 自分でそのパッケージのURLとハッシュ値を調べてresourceに追加する必要があります。

Formulaが出来たら

1
$ brew install cocoro

としてインストールできます。

インストールされたもの

1
2
$ which cocoro
/opt/homebrew/bin/cocoro

こんな感じでコマンドがHomebrewのbinディレクトリにインストールされています(これ自体はシンボリックリンク)。 (opt~の部分は環境によって違う、$(brew --prefix)/bin/cocoro)

中身は

cocoro
1
2
3
4
5
6
7
8
#!/opt/homebrew/Cellar/cocoro/0.1.4/libexec/bin/python3.11
# -*- coding: utf-8 -*-
import re
import sys
from cocoro import main
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(main())

となっていて、これは通常通りpip installで入れた場合と内容は基本的に同じです。

ただ、Shebangを見てみると

1
#!/opt/homebrew/Cellar/cocoro/0.1.4/libexec/bin/python3.11

となっていて、このpythonは /opt/homebrew/Cellar/cocoro/0.1.4/libexec/に作られたPythonの仮想環境のものになっています。

この仮想環境を直接使うことは通常はないかと思いますが、

1
$ source /opt/homebrew/Cellar/cocoro/0.1.4/libexec/bin/activate

で入ることも出来ます。

こんな感じで各Fomrula毎に仮想環境を用意してその中に必要なパッケージを入れて実行しているので 他のPython環境と競合せずに管理できます。

ちょっと注意が必要なのはHomebrewでインストールしたPythonのpipコマンドを使って同じツールをインストールすると 同じく/opt/homebrew/bin/cocoroとしてインストールされるので 競合してしまいます。

後からbrewで入れると

1
Error: The `brew link` step did not complete successfully

みたいに出てbinへの実行ファイルのリンクが作れません。 仮想環境やリンク元のファイルは出来ているので、pip uninstall cocoroとしてからbrew link cocoroするか brew link --overwrite cocoroで矯正上書きしてしまうかで置き直せます。

インストール時間

上のcocoroはそれほど依存パッケージを持っていませんが、 それでもbrew installで入れると普通にpip installするよりも結構時間がかかりました。

pipなら1,2秒のところ(空の仮想環境に入れてみるテスト)、 brew installだと5分位かかりました。

1
==> /opt/homebrew/Cellar/cocoro/0.1.4/libexec/bin/pip install -v --no-deps --no-binary :all: --use-feature=no-binary-enable-wheel-cache --ignore-installed /tmp/cocoro--termcolor-20230412-26839-193n1qy/termcolor-2.2.0.tar.gz

の部分がやけに時間かかったみたいで、--no-binaryでビルドするようになってるのでパッケージによっては時間がかかるものが出てきます。 他のものはそこまで時間はかかってないので殆どの場合ではbrewでも数秒から数十秒程度で終わると思います。 (仮想環境作ったりパッケージビルドしたりするので多少は時間がかかる。)

単純に仮想環境でpip install <package>==<version>で依存関係も含めて全部入れてくれるような オプションもあっても良いな、とは思います。

それだとインストールが速いのと、Formulaもurlの代わりにcocoroなどを指定するpackage、 それとバージョン指定のversionを書くだけでシンプルになってresorceも要らなく出来ます。

ただ、Python for Formula Authors — Homebrew Documentation を見るとそういうのは思想に反する感じではありますが。

パッケージのアップデート

上にあるように各パッケージはすべてバージョン指定された形でそのソースファイルのURLとハッシュが指定されていますが、 これを手でアップデートするのはかなり大変です。

そんなときのためのコマンドがbrew update-python-resources

これ使うにあたって pipgripというツールが必要なのでこれを別途

1
$ brew install pipgrip

としてインストールしておく必要があります。

これで

1
$ brew update-python-resources cocoro

とすると各パッケージのバージョンの再確認を行ってくれます。

このツールを使うに、

これはHomebrewのTapの中にないものでも直接適用することが出来て、 カレントディレクトリにcocoro.rbがあれば

1
$ brew update-python-resources ./cocoro.rb

とすればそのファイルをアップデートしてくれます。

これを使えば、

cocoro.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Cocoro < Formula
  include Language::Python::Virtualenv

  desc "Tools for COCORO API (SHARP products)."
  homepage "https://pypi.org/project/cocoro/"
  url "https://files.pythonhosted.org/packages/33/3a/7ada2648654931f7b01885eb14eb0d7988115247146ece39fc6b1cf0ff6a/cocoro-0.1.4.tar.gz"
  sha256 "170404bfe64c80e6742689225995a23708855f445dd7b65ff690ad7f2278d417"
  license "Apache-2.0"

  depends_on "python"


  def install
    virtualenv_install_with_resources
  end
end

みたいな感じの最小限のformulaを書いてbrew update-python-resourcesを適用することでもFormulaを完成させられます。

もとの cocoro-0.1.4.tar.gzのハッシュ値は自分で求めないといけませんが、個人的には 以下のようなスクリプトを使ってURL先のハッシュ値を取得できるようにしています。

配布

出来たら $(brew --repo rcmdnk/testtap)/cocoro.rb をコピーして、配布用のhomebrew-mypackagesみたいなtapレポジトリに追加してGitHubに上げれば

1
$ brew install <github_user>/mypackages/cocoro

でインストールできるようになります。

まとめ

pipだけで配布できるものをわざわざHomebrewを使って、 ということで配布側にはちょっと手間がかかりますが、 ユーザー側としてはHomebrewさえ使っていれば 既存のPython環境を汚さずに、また、Pythonのバージョンの指定も含めて インストールするように出来るのは結構便利ではないかな、と。

特にアップデートがあった際にpipだと直接そのパッケージをアップデートするコマンドを打たないといけませんが、 Homebrewであれば定期的にbrew update && brew upgradeとかしておけば 他のパッケージ含めてアップデートしてくれるので楽です。

また、PyPIに公開されているPythonのコマンドラインツールであればどれでも同じことが出来るので、 自分のパッケージでなくても自分の管理用にFormulaを作ってHomebrewで管理できるようにする、という手段もありかと思います。

Sponsored Links
  1. warning: You appear to have cloned an empty repository.という注意が出ますが brew createでファイルを追加テストする分には 問題なく動きます。

Sponsored Links

« SlackでChatGPTによるWebページの要約を頼むコマンドをGoogle Apps Scriptで作る chatgpt-prompt-wrapper: コマンドラインから気軽にChatGPTを使う »

}