Python製自作ツールをHomebrewでインストール可能にする

## 動機

たとえば`randstr`というスクリプトを`python`で実装したとする．
動作としては，与えられた引数の長さだけランダムな文字列を出力する．

```bash
$ python ./randstr.py 5
4UJrG
$ python ./randstr.py 10
1sHGTmjiAf
```

これを最終的に以下でインストール・配布できるようにしたい．

```bash
$ brew tap scstechr/ranstr
$ brew install randstr
```

## 全体の流れ
本記事はMacを前提にしてますが，流れ自体は他OSでも変わらないと思います．

1. `pyinstaller`で実行ファイル（バイナリ）作成
2. `git`レポジトリを作成しGitHubに`push`
3. 実行ファイルなどを含む`.tar.gz`を作成
4. 作成した`.tar.gz`ファイルを添付したリリースをパブリッシュ
5. `brew create`で`Formula`を作成
6. 作成した`Formula`をGitHub上の`homebrew-<name>`に`push`


## 1. `pyinstaller`で実行ファイル（バイナリ）作成
### 実装
`randstr`は以下のソースコードで実装されている．

```python
import random
import string
import click

def randomname(n):
   randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)]
   return ''.join(randlst)

@click.command()
@click.argument('length')
def main(length):
    try:
        length = int(length)
        randlst = [random.choice(string.ascii_letters + string.digits) for i in range(length)]
        print(''.join(randlst))
    except:
        print('./randstr [length]')


if __name__ == '__main__':
    main()
```

### 実行ファイルの作成
まず`pyinstaller`を使ってバイナリファイルを作成する． 
`pyinstaller`が入っていなければ`pip install pyinstaller`で入れる．

```bash
$ pyinstaller randstr.py --onefile
```

これで`build/`, `dist/`, `randstr.spec`ができる.

```bash:ファイル構成
.
├── .git/
├── README.md
├── build/
├── dist/
│   └── randstr
├── randstr.py
└── randstr.spec
```

`dist`ディレクトリには実行形式の`randstr`が入ってるので試しに実行してみる．

```
$ ./dist/randstr 5
FJCgc
$ ./dist/randstr 10
38cuZYR9xF
```

## 2. `git`レポジトリを作成しGitHubに`push`
### ローカル
まず，`git init`してレポジトリを作成する．
次に`.gitignore`を作成して`randstr.spec`がコミットに追加されないようにする．
任意だが`dist/`,`build/`も必要なければ`.gitignore`に追記する．

```:.gitignore
.*
*.spec
build
dist
```
`git add -f .gitignore`で`.gitignore`を強制的に`add`する．

```bash:ファイル構成
.
├── .git/
├── .gitignore
├── README.md
├── build/
├── dist
│   └── randstr
├── randstr.py
└── randstr.spec
```

次に`git add .`, `git commit`で`commit`する．

### リモート
GitHubにレポジトリを作成し，先ほど作成したものを`push`する．  
![GitHubのレポジトリ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/0e4f6537-8215-1b46-7147-dfb98a50c166.png)

## 3. 実行ファイルなどを含む`.tar.gz`を作成
ここで作成するのは`*.tar.gz`という実行バイナリを含む`tar.gz`ファイルである．
今回の例では`randstr-v1.0.tar.gz`を作成する．

まず, `randstr-v1.0`ディレクトリを作成して，`dist/randstr`，`README.md`を`cp`する．
ほかに`LICENCE`など入れたいファイルがあれば追加する．

```
.
├── .git/
├── .gitignore
├── README.md
├── build/
├── dist
│   └── randstr
├── randstr-v1.0/
│   ├── README.md
│   └── randstr
├── randstr.py
└── randstr.spec
```

これを`tar.gz`に圧縮する．  

```bash
$ tar zcvf randstr-v1.0.tar.gz randstr-v1.0
```

ついでに`tar.gz`も`.gitignore`に追加しておく．

```:.gitignore
.*
*.spec
build
dist
*.tar.gz
```

#### バッチファイルの作成

ここまでの処理を`build.sh`などのスクリプトを書いておくと楽．

```bash
#!/bin/bash

printf '\e[34mmkdir release\e[m\n';
mkdir release;
printf '\e[34mcp README.md -> release\e[m\n';
cp README.md release;
printf '\e[34mpyinstaller gch\e[m\n';
pyinstaller randstr.py --onefile && mv dist/randstr release/
printf '\e[34mtar zcvf\e[m\n';
tar zcvf release.tar.gz release
printf '\e[34mrm -rf build, dist, release\e[m\n';
rm -rf build
rm -rf dist
rm -rf release
printf '\e[34mrm spec filest\e[m\n';
rm *.spec
```

上の`build.sh`を`chmod u+x build.sh`後に`./build.sh`とすると
実行ファイルの作成，`README.md`と`dist/randstr`を`release.tar.gz`に圧縮するまでをやってくれる．
あとは`mv release.tar.gz randstr-v1.0.tar.gz`などしてあげれば良い.

```
.
├── .git/
├── .gitignore
├── README.md
├── randstr.py
└── randstr-v1.0.tar.gz
```

## 4. 作成した.tar.gzファイルを添付したリリースをパブリッシュ

GitHubに戻って， `Release`をクリックし `Create a new release`を選択する．
![リリース](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/5496ca13-6c5a-a9d9-d650-855566825cf1.png)

リリースノードを作成し，先ほど作成した`randstr-v1.0.tar.gz`を添付する．
`Publish release`をクリックすれば無事パブリッシュされる．

![パブリッシュ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/d747073a-e433-a70d-66d8-bcead83e2f71.png)

## 5. `brew create`で`Formula`を作成
まず，　レポジトリ`homebrew-<名前>`(今回は`homebrew-randstr`)をGitHubで作成する．
これを好きなところに`git clone`しておく． 今回は`~/Works/`に`clone`したとする．

```bash
$ git clone https://github.com/Scstechr/homebrew-randstr
Cloning into 'homebrew-randstr'...
warning: You appear to have cloned an empty repository.
```
```:~/Works/homebrew-randstr
.
└── .git/
```

次に，先ほどパブリッシュしたリリースに添付した実行ファイル（今回の例では`randstr-v1.0.tar.gz`）のリンクをコピーする．
![URL](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183420/231e68c3-97c1-856a-4ec4-8498935e13dc.png)
そして，`brew create <コピーしたURL>`をターミナルで実行する．

```
$ brew create https://github.com/Scstechr/Randstr/releases/download/v1.0/randstr-v1.0.tar.gz
```

これを実行すると， `Formula`と呼ばれるものが`/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/randstr.rb`として(細かいディレクトリは違うかもしれないが)作成される． 
すぐに編集できるように自動的にVimが開くが，これ一旦`:wq`で抜ける．
作成された`randstr.rb`を`<先ほどgit cloneした先>/randstr.rb`に`mv`する.

```
mv /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/randstr.rb ~/Works/homebrew-randstr/
```

これで`~/Works/homebrew-randstr`のファイル構成が以下の通りとなる．

```:~/Works/homebrew-randstr
.
├── .git/
└── randstr.rb
```

改めて開いてみると，`randstr.rb`はこんなファイルのはずである．

```randstr.rb
# Documentation: https://docs.brew.sh/Formula-Cookbook
#                https://rubydoc.brew.sh/Formula
# PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!
class Randstr < Formula
  desc "Random String Generator"
  homepage ""
  url "https://github.com/Scstechr/Randstr/releases/download/v1.0/randstr-v1.0.tar.gz"
  sha256 "5c3b72ae94a11d85b4cf460595c64336ad9985dc976a9daf8dc33a0cbf5c9cb8"
  # 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! For Homebrew/homebrew-core
    # this will need to be a test that verifies the functionality of the
    # software. Run the test with `brew test Randstr`. 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
```

上記のファイルを下記のように書き換えて自動生成されたコメントを消す．

```randstr.rb
class Randstr < Formula
  desc "Random String Generator"
  homepage "https://github.com/Scstechr/Randstr"
  url "https://github.com/Scstechr/Randstr/releases/download/v1.0/randstr-v1.0.tar.gz"
  sha256 "5c3b72ae94a11d85b4cf460595c64336ad9985dc976a9daf8dc33a0cbf5c9cb8"

  def install
     bin.install 'randstr'
  end
end
```

ここで，`bin.install 'randstr'`はパスが通ってるディレクトリにシンボリックリンクを貼ってくれる．
細かい`Formula`の書き方は他の記事を参照する．

## 6. 作成した`Formula`をGitHub上の`homebrew-<name>`に`push`
`git commit`で変更を反映したあと，`git push`でリモートレポジトリに`push`する．

# 完成

ここまでくれば，まず`brew tab scstechr/randstr`で`tap`し

```bash
$ brew tap scstechr/randstr
==> Tapping scstechr/randstr
Cloning into '/usr/local/Homebrew/Library/Taps/scstechr/homebrew-randstr'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
Tapped 1 formula (27 files, 23.5KB).
```
`brew install randstr`で`randstr`をインストールできる．

```
$ brew install randstr
==> Installing randstr from scstechr/randstr
==> Downloading https://github.com/Scstechr/Randstr/releases/download/v1.0/randstr-v1.0.tar.gz
🍺  /usr/local/Cellar/randstr/1.0: 4 files, 6.2MB, built in 5 seconds
```


また，どこでも`randstr`が実行できるようになった．

```
$ randstr 5
FNsB3
$ randstr 10
Wtgk8d9ABb
```

以上より，下記のコマンドで`randstr`が誰でも簡単にインストールできるようになった．

```
$ brew tap scstechr/ranstr
$ brew install randstr
```