GitHub Releasesから実行可能ファイルをダウンロードするCLIツールを作った

長らくZshプラグインマネージャとして使用してきたZinitからsheldonへと移行するため、GitHub Releasesから実行可能ファイルをダウンロード・インストールするCLIツール gh-rd を Deno で作成しました。

github.com

これは何?

TypeScriptの設定ファイルでGitHub ReleasesからインストールするCLIツールを管理できるツールです。

// $XDG_CONFIG_HOME/gh-rd/config.ts
import { defineConfig } from "https://raw.githubusercontent.com/Ryooooooga/gh-rd/main/src/config/types.ts";

export default defineConfig({
  tools: [
    {
      name: "rossmacarthur/sheldon",
    },
    {
      name: "BurntSushi/ripgrep",
    },
    {
      name: "neovim/neovim",
      enabled: Deno.build.arch === "x86_64",
    },
  ],
});
# .zshrc
# PATHを通しておく
path=(
  "$XDG_DATA_HOME/gh-rd/bin"(N-/)
  "$path[@]"
)
fpath=(
  "$XDG_DATA_HOME/gh-rd/completions"(N-/)
  "$fpath[@]"
)
$ gh-rd
Installing packages
rossmacarthur/sheldon    Install completed (0.7.2)
BurntSushi/ripgrep       Install completed (13.0.0)
neovim/neovim            Install completed (stable)

ツールの更新も同様のコマンドで行えます (すでに最新リリースのものがインストールされている場合はインストールがスキップされます)。

$ gh-rd
Installing packages
rossmacarthur/sheldon    Install completed (0.7.3)
BurntSushi/ripgrep       Up to date (13.0.0)
neovim/neovim            Up to date (stable)

また、インストール時・更新時に任意の処理を実行できる onDownload hookがあるため、補完の生成などはそのタイミングで行えます。

// $XDG_CONFIG_HOME/gh-rd/config.ts
import { defineConfig } from "https://raw.githubusercontent.com/Ryooooooga/gh-rd/main/src/config/types.ts";

async function saveCommandOutput(
  cmd: [string, ...string[]],
  to: string,
) {
  const { stdout } = await new Deno.Command(cmd[0], {
    args: cmd.slice(1),
    stderr: "inherit",
  }).output();

  await Deno.writeFile(to, stdout);
}

export default defineConfig({
  tools: [
    {
      name: "cli/cli",
      async onDownload({ packageDir, bin: { gh } }) {
        // 補完の生成: gh completion --shell zsh >_gh
        await saveCommandOutput(
          [gh, "completion", "--shell", "zsh"],
          `${packageDir}/_gh`,
        );
      },
    },
    {
      name: "direnv/direnv",
      rename: [
        { from: "direnv*", to: "direnv", chmod: 0o755 },
      ],
      async onDownload({ packageDir, bin: { direnv } }) {
        // 初期化スクリプトのキャッシュ: direnv hook zsh >direnv.zsh
        await saveCommandOutput(
          [direnv, "hook", "zsh"],
          `${packageDir}/direnv.zsh`,
        );
      },
    },
  ],
});

私の運用している設定は以下のdotfilesから参照できます。

github.com

経緯

Zinitをやめた理由

2021年にZinitの作者であるpsprint氏がZinitのリポジトリをzdharma organization ごと爆破して以降 *1zdharma-continuum/zinit を使用してきました。

直近のZinitはログのスタイルが崩れるなど動作の安定性に欠けていたのですが、zdharma-continuum/zinit に対してコントリビューションを続けていたpsprint氏が Zinit 4 なるforkを作成したことでいよいよ諦めてsheldonへと乗り換える決心が付きました。

実のところsheldonへの乗り換えは今まで何度も検討していたのですが、その障害となっていたのがGitHub Releasesからインストールしている様々なモダンCLIツールの存在でした。 ZinitにはGitHub Releasesから実行可能ファイルをインストールする機能 ( from"gh-r" ) があるのですが、sheldonにはそれに類する機能がありません (Issue はありますが放置されています)。

代替手段の検討

自分のdotfilesはmacOSUbuntu (on WSL2) をターゲットとしており、それらの環境双方で使用できるなんらかの代替手段が必要になりました。 macOSについては自作ツールを含め Homebrew でほぼ全ての必要なツールを管理できますが、aptは対応しているツールの数が極端に少ないため代替にはなり得ません。

そこで以下に示すいくつかの手段の使用を検討しました。

Linuxbrew

HomebrewはLinuxにも対応していますが、Rubyの導入が必要なため諦めました。

aqua

aqua は有力な候補でしたが、aquaはツールバージョンの固定が必須になっています *2。 その点が基本的にツールを最新バージョンに維持しておきたいdotfilesの要求と噛み合いませんでした。

github.com

ちなみにRenovateを導入するとバージョン更新をほぼ自動化できますが、dotfilesのコミットログがRenovateで埋め尽くされるのも体験が悪くなることが予想できたため最終的に使用を断念しました。

この通り、需要を満たしてくれるツールが見つからなかったため、結局自作することにしました。

まとめ

これを作成したことでどうにかZinitからsheldonへの宗旨替えに成功しました。

*1: 現在のzdharmaはdomain spoofingです。z-shell/zi はZinitのフォークですが倫理観の無いカスなので使用してはいけません。

*2:Support building tools with Go | aqua