GitHub Releases の CLI を TOML で一括管理するツールを作った

# GitHub Releases の CLI を TOML で一括管理するツールを作った

この記事は [Go 3 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/go3) の 20 日目の記事です。

## TL;DR

GitHub Releases にアップロードされているバイナリを自動でインストールする CLI を作ったので布教します。

![demo](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506317/3378d30d-21d1-4ad3-ab6b-40e3920eec8b.gif)

## はじめに

みなさん、CLI は好きですか？ぼくは好きです。
キーボードから手を離さなくていいし、やりたいことをすぐできるし、何よりカッコイイ。

とくに最近は Go や Rust といった言語の流行に伴って、Unix のコマンドを再実装した [bat](https://github.com/sharkdp/bat) や [exa](https://github.com/ogham/exa) だったり、今年は GitHub 公式から [gh](https://github.com/cli/cli) コマンドも正式にリリースされました。

これまで多かった Node.js や Python 製の CLI と比べて、Go や Rust で実装されたこれらの CLI は事前にビルドされて配布されているので、ランタイムに依存しないという点でも環境に優しいです。

そんな便利な CLI たちですが、ある時ぼくは管理が面倒という問題に直面しました。
何が面倒かというと、毎回「Web サイトから適切なアセットをダウンロード -> 圧縮ファイル解凍 -> バイナリをパスに移動 -> 残骸を削除」をやらなくてはいけないということでした。
まあ 100 歩譲ってこれを許したとして、アップグレードを行うためにはもう一度同じ動作を踏まなければなりませんし、そもそもアップグレードがあったことを受動的に知る方法がありませんので、困りました。

ここで Mac を使っている人は Homebrew で済むのかもしれませんが、Linux しか使わないぼくは脳死でインストールするツールを持ち合わせていませんでした。
Linuxbrew をサポートしているかはツールによるし、apt や pacman といったツールはシステムグローバルにインストールしてしまうので個人的には論外でした (そもそもこれらで配布されていないケースも多いと思います)。

また、[Songmu/ghg](https://github.com/Songmu/ghg) のようなツールはぼくの欲しい機能がありましたが、1 つのアプリケーションずつのインストールにしか対応していなかったのが不満でした。
さらに、この Go 3 Advent Calendar の 10 日目に [同じようなツールを作られた記事](https://qiita.com/jiro4989/items/03dbe5a94a427ab42e3c) が上がっていて、タイトルを見たときはネタが被った！と思って肝を冷やしたのですが、どうやら ghg 同様に一括の管理には対応していないようでした。

こんなぼくの不満を解決する便利なコマンド管理ツールを作ったので布教します！

## 作ったもの

[![skmatz/vin - GitHub](https://gh-card.dev/repos/skmatz/vin.svg)](https://github.com/skmatz/vin)

コマンド名である `vin` は、バイナリ (bin) を管理するというところから文字って付けました。
基本的に Linux で使うことを想定していますが、知人の Mac と VM の Windows で動作は確認できています。

### インストール

[GitHub Releases](https://github.com/skmatz/vin/releases) から自分のマシンにあったものを選んで解凍した後にパスの通ったディレクトリに入れてください。
残念ながら最初のインストールはブラウザを開いて面倒な手順を踏む必要があるのですが、まあこれが最後になると思って、、、

インストールが終わったら、`vin` によってインストールした CLI へのパスを通すために、次のコマンドを実行する必要があります。

```bash
export PATH="$HOME/.vin/bin:$PATH"
```

### 使い方

冒頭に述べたように、モチベーションは「CLI を一括で管理したい」ということでしたので、TOML ベースで管理するようにしました。
イメージは Vim のプラグインマネージャである [Shougo/dein.vim](https://github.com/Shougo/dein.vim) で、TOML で管理することで dotfiles にでも入れておけばすぐに環境の再現ができます。

まだ開発中ではありますが、文法は至ってシンプルで、

```toml
# ~/.config/vin/vin.toml
[[app]]
repo = "cli/cli"
```

これだけです。
この例では <https://github.com/cli/cli> にホストされている `gh` コマンドを表しています。
下のコマンドを実行することで上の例で示した TOML ファイルを規定のパスに生成できます。

```bash
# darwin
mkdir -p ~/Library/Application\ Support/vin
vin example > ~/Library/Application\ Support/vin/vin.toml

# linux
mkdir -p ~/.config/vin
vin example > ~/.config/vin/vin.toml

# windows (confirmed on Windows Terminal)
mkdir -p ~\AppData\Roaming\vin\vin.toml
vin example > ~\AppData\Roaming\vin\vin.toml
```

あとは

```bash
vin get
```

とするだけで、GitHub Releases の最新のリリースから、適切なバイナリを抽出してくれます。

### 仕組み

一応簡単に仕組みを書いておくと、GitHub Releases には下図のように複数のアセットが並んでいます。

![assets](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506317/b648679c-dd5d-6074-d43f-2cd7d820aabb.png)

この中から使っているマシンに適当なアセット (ここでは圧縮ファイルを想定) をダウンロードするわけですが、Vin はファイル名を参考にアセットを自動で選択します。
より具体的には、Go 言語には `GOOS` と `GOARCH` というランタイムの情報を取得できる runtime パッケージがあるので、それを含んでいるファイルを抽出するような仕組みにしています。

適切なアセットを選んだら、それをダウンロード -> 解凍した後に、実行可能ファイルの探索を行います。
Mac か Linux の場合であれば各ファイルの権限を見て、Windows の場合は `.exe` suffix が付いているかを見て判断しています。

これによって実行ファイルが判定でき、最後にバイナリファイルを指定のパスに移動させるという実装になっています。
もちろんダウンロードした圧縮ファイルや解凍したディレクトリはプログラムが終わるタイミングですべて消去されます。

### 設定

2020/12/17 現在 (v0.3.2) で設定可能な項目は次のとおりです。

```toml
# ~/.config/vin/vin.toml
[[app]]
repo = "cli/cli"

tag = "v1.2.0"

keywords = ["amd64", "linux"]

name = "gh"

hosts = ["awesome-machine"]

priority = 3

command = """
gh completion -s zsh > ~/.zfunc/_gh
"""
```

<details><summary>設定例はこちら</summary><div>

```toml
[[app]]
repo = "aquasecurity/trivy"
keywords = ["linux", "64bit"]

[[app]]
repo = "b4b4r07/gomi"
keywords = ["linux", "x86_64"]

[[app]]
repo = "cli/cli"
command = """
gh completion -s zsh > ~/.zfunc/_gh
"""

[[app]]
repo = "client9/misspell"
keywords = ["linux"]

[[app]]
repo = "extrawurst/gitui"
keywords = ["linux", "musl"]

[[app]]
repo = "goreleaser/goreleaser"
keywords = ["linux", "x86_64"]

[[app]]
repo = "itchyny/mmv"

[[app]]
repo = "moncho/dry"
name = "dry"

[[app]]
repo = "nektos/act"
keywords = ["linux", "x86_64"]

[[app]]
repo = "ogham/exa"
keywords = ["linux", "x86_64"]
name = "exa"

[[app]]
repo = "Rigellute/spotify-tui"
keywords = ["linux"]

[[app]]
repo = "sharkdp/bat"
keywords = ["x86_64", "linux", "musl"]

[[app]]
repo = "sharkdp/fd"
keywords = ["x86_64", "linux", "musl"]

[[app]]
repo = "skanehira/docui"
keywords = ["linux", "x86_64"]

[[app]]
repo = "skmatz/github-remove-repository"
keywords = ["linux", "x86_64"]
command = """
ghrm completion -s zsh > ~/.zfunc/_ghrm
"""

[[app]]
repo = "skmatz/git-bump"
keywords = ["linux", "x86_64"]

[[app]]
repo = "skmatz/spotify-cli"
keywords = ["linux", "x86_64"]

[[app]]
repo = "skmatz/spotify-skip-instrument"
keywords = ["linux", "x86_64"]

[[app]]
repo = "skmatz/vin"
keywords = ["linux", "x86_64"]
command = """
vin completion -s zsh > ~/.zfunc/_vin
"""

[[app]]
repo = "skmatz/zscroll-go"
keywords = ["linux", "x86_64"]
command = """
zscroll completion -s zsh > ~/.zfunc/_zscroll
"""

[[app]]
repo = "stedolan/jq"
keywords = ["linux64"]
name = "jq"

[[app]]
repo = "x-motemen/ghq"
```

</div></details>

#### repo

`repo` は "owner/repo" フォーマットの GitHub のリポジトリ名です。

#### tag

tag は GitHub のコミットタグです。
空の場合は最新のリポジトリを自動で探してきます。

#### keywords

keywords はアセットを検索して絞り込むためのキーワードのリストです。
空の場合は `GOOS` と `GOARCH` から自動で検索しますが、アセットの名前は任意なので、たとえば `amd64` の代わりに `x86_64` と名付けられているアセットをインストールしたい場合は `["linux", "x86_64"]` のようにキーワードを設定することで対応できます。

#### name

name は CLI のファイル名です。
基本的には設定しないことでデフォルトのファイル名が適応されますが、たとえば [exa](https://github.com/ogham/exa) のアセットには `exa-linux-x86_64` という名前のバイナリファイルが含まれているので、これを変更したい時に設定できます。

#### hosts

hosts はインストールするホストを制限するためのリストです。
デフォルトではすべてのホストでインストールされますが、このアプリケーションはこの PC にのみインストールしたいという場合に設定すると対応できます。

#### priority

priority はアプリケーションの優先度を表す指標です。
インストールする際に引数として最低限の priority を指定することで、その priority 以上のアプリのみをインストールできます。

#### command

command はインストール終了後に実行するコマンドを表します。
基本的には completion を自動で吐くために用意したものですが、一応任意の操作ができます。

### コマンド

#### get

主な機能はこのコマンドに集約されています。

```bash
vin get
```

基本的にはこれだけで、TOML ファイルに記述されているすべてのアプリケーションをインストールします。
ただ、インストール時にタグ情報をキャッシュしておく機能がデフォルトではオンになっているので、2 回目以降は最後のインストールからバージョンが変わったもののみをインストールしてくれます (これを無視するには `--ignore-cache` オプションを付けます)。

#### token

CLI の数が膨大になると GitHub の API 制限を食らうことがあります。
その場合は

```bash
vin token
```

と叩き、表示にしたがってアクセストークンを取得すれば回避できます。

その他のコマンドや各コマンドのオプションは `--help` を参考にしてください。

## まとめ

最後まで呼んで頂きありがとうございました！
ムダな CLI を作ることに定評のあるぼくにしては実用的なアプリケーションを作れた気がします。
もう少し入れたい機能と修正したい箇所があるので、年末年始にでも取り掛かっていけたらいいかなと思っています。