Homebrewを利用して社内ツールを配布する

こんにちは!スタッフエンジニアの @kenkoooo です。今回は、Homebrew を使って社内ツールを簡単に配布する方法を紹介します。

社内ツール開発

社内で使用する便利ツールを開発する方法はいくつかあります。

  • 社員だけがアクセスできるウェブアプリ
  • ブラウザの拡張機能
  • ダウンロードしてローカルで動かす実行可能ファイル
  • etc.

どのPCにもブラウザはインストールされているため、ユーザーに実行環境を用意する負担をかけないという点では、ウェブアプリやブラウザ拡張機能が適しているでしょう。一方で、ブラウザで動作しないローカル開発用のCLIツールなど、実行可能ファイルを配布するしかない場合もあります。

CLIツールを配布する際、毎回「tar.gzをダウンロードして展開し、出てきたファイルをPATHが通っている場所に置いてください」と指示するのは手間がかかりますし、こういった作業に慣れていない社員もいるでしょう。コマンド一つでインストールできるのが理想です。

Homebrew

macOS では、サードパーティ製ツールをインストールするために Homebrew というパッケージマネージャが広く使われています。公式のツールではありませんが、エンジニアで Mac を使っている人の大半がインストールしているのではないでしょうか。

Homebrew を使うことで、サードパーティのツールをコマンド一発でインストールできます。Homebrew のパッケージの定義は Formula と呼ばれる Ruby ファイルに書かれていて、デフォルトの設定では公式リポジトリに Formula があればインストールできます。このリポジトリにプルリクエストを出して新しい Formula を追加することもできます。

このような Formula を集めたリポジトリを tap と言います。Homebrew では、デフォルトの tap の他にサードパーティの tap を追加することができます。有名なものでは、AWS の aws/homebrew-tap があります。この tap を追加し、aws/copilot-cli というツールをインストールするのは以下のようにしてできます。

# brew tap [tap の GitHub リポジトリ名] で tap を追加できます
# 先頭の homebrew- は省略できるので aws/homebrew-tap と aws/tap は同じです
brew tap aws/tap

# brew install [tap 名]/[ツール名] でツールをインストールできます
# aws/tap 内の copilot-cli というツールは以下のようにインストールできます
brew install aws/tap/copilot-cli

このように、配布したいツールの情報を記載した Formula と、それを配信するための tap を用意することで、Homebrew ユーザーにツールを配布することができます。ユーザー側はその tap を追加しておけば、brew コマンド一発でツールを簡単にインストールできます。

GitHub プライベートリポジトリからの配布

上記の方法で自作のツールを配布することで、brew コマンドでインストールできるようになりますが、社内ツールを配布する場合、tap もツールもプライベートリポジトリにあるはずです。GitHub プライベートリポジトリからツールを配布するにはひと工夫必要になります。

[ユーザー側] Personal Access Token の設定

Homebrew の実行時に、GitHub の Personal Access Token を環境変数に設定するようにします。これは以下のステップで行います。

  1. GitHub から Personal Access Token を取得します。

  2. 取得したトークンを HOMEBREW_GITHUB_API_TOKEN として環境変数に設定します。

この設定を行うと、brew コマンドが Personal Access Token を使って GitHub にアクセスするようになります。これで自作の tap を brew tap コマンドで追加できます。

[配布側] Formula の作成

配布するツールの Formula を作成します。ファイルをダウンロードしてきてインストールする Formula は、大まかに以下のような形式です。

class MyCoolTool < Formula
    version <バージョン番号>
    url <ダウンロードするファイルのURL>
    sha256 <ダウンロードするファイルのSHA256チェックサム>

    def install
        bin.install <インストールする実行可能ファイルの名前>
    end
end

url と sha256 を指定することで、ファイルをダウンロードしてインストールすることができます (詳細はこちら)。また、 bin.install を使うことで実行可能権限をつけて bin ディレクトリに入れてくれます (詳細はこちら)。

ここで配布するツールは my-cool-tool という名前で、以下のようなものとします。

  • my-cool-tool という実行可能ファイルが tar.gz に固められている。
  • Linux (x86) 用と macOS (ARM) 用の2種類の実行可能ファイルが用意されている。
  • 2種類の .tar.gz ファイルは両方とも GitHub Releases にアップロードされている。

これらの条件下でツールを配布する Formula は次のようになります。

require_relative './lib/strategy' # GitHubPrivateRepositoryReleaseDownloadStrategy を読み込む

class MyCoolTool < Formula
  version 'v0.1'

  on_macos do
    if Hardware::CPU.arm?
      url 'https://github.com/estie-inc/my-cool-tool/releases/download/v0.1/my-cool-tool-macos-arm64.tar.gz',
          using: GitHubPrivateRepositoryReleaseDownloadStrategy
      sha256 'f0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde'

      def install
        bin.install 'my-cool-tool'
      end
    end
  end

  on_linux do
    if Hardware::CPU.intel?
      url 'https://github.com/estie-inc/my-cool-tool/releases/download/v0.1/my-cool-tool-linux-amd64.tar.gz',
          using: GitHubPrivateRepositoryReleaseDownloadStrategy
      sha256 '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'

      def install
        bin.install 'my-cool-tool'
      end
    end
  end
end

OS および CPU アーキテクチャごとに条件分岐し、それぞれ異なる URL からファイルをダウンロードしてインストールしています。

url を指定してファイルをダウンロードする際に、using でStrategy を指定することで、必要な処理を組み込むことができます。プライベートリポジトリの Releases からファイルをダウンロードするため GitHubPrivateRepositoryReleaseDownloadStrategy という自前のクラスを作成し、これを使用しています。こちらの中身は次のようになっています。

class GitHubPrivateRepositoryReleaseDownloadStrategy < CurlDownloadStrategy
  require 'utils/formatter'
  require 'utils/github'

  def initialize(url, name, version, **meta)
    super
    parse_url_pattern
    set_github_token
  end

  private

  def _fetch(url:, resolved_url:, timeout:)
    curl_download download_url, '--header', 'Accept: application/octet-stream', to: temporary_path, timeout: timeout
  end

  def download_url
    "https://#{@github_token}@api.github.com/repos/#{@owner}/#{@repo}/releases/assets/#{asset_id}"
  end

  def parse_url_pattern
    url_pattern = %r{https://github.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(\S+)}
    raise CurlDownloadStrategyError, 'Invalid url pattern for GitHub Release.' unless @url =~ url_pattern

    _, @owner, @repo, @tag, @filename = *@url.match(url_pattern)
  end

  def set_github_token
    @github_token = ENV['HOMEBREW_GITHUB_API_TOKEN']
    unless @github_token
      raise CurlDownloadStrategyError, 'Environmental variable HOMEBREW_GITHUB_API_TOKEN is required.'
    end

    validate_github_repository_access!
  end

  def validate_github_repository_access!
    # Test access to the repository
    GitHub.repository(@owner, @repo)
  rescue GitHub::HTTPNotFoundError
    # We only handle HTTPNotFoundError here,
    # because AuthenticationFailedError is handled within util/github.
    message = <<~EOS
      HOMEBREW_GITHUB_API_TOKEN can not access the repository: #{@owner}/#{@repo}
      This token may not have permission to access the repository or the url of formula may be incorrect.
    EOS
    raise CurlDownloadStrategyError, message
  end

  def asset_id
    @asset_id ||= resolve_asset_id
  end

  def resolve_asset_id
    release_metadata = fetch_release_metadata
    assets = release_metadata['assets'].select { |a| a['name'] == @filename }
    raise CurlDownloadStrategyError, 'Asset file not found.' if assets.empty?

    assets.first['id']
  end

  def fetch_release_metadata
    GitHub.get_release(@owner, @repo, @tag)
  end
end

Homebrew のライブラリとして、URL を指定してファイルをダウンロードする CurlDownloadStrategy が用意されているので、これを拡張することにします。

先ほどの Formula で Releases のブラウザ用 URL を指定していましたが、GitHub API 経由で Releases からファイルをダウンロードするのにはこの URL を使うことができません。そこで、この Strategy 内では与えられたブラウザ用の URL をパースして必要な情報を抜き出し、さらに GitHub API にアクセスして API アクセス用の URL を組み立てるのに必要な Asset ID というのを取得し、これらを組み合わせて API アクセス用の URL を組み立ててからダウンロードしています。API 用の URL に Personal Access Token も必要なので、 HOMEBREW_GITHUB_API_TOKEN からトークンを取り出して URL に含めます。

この自前の Strategy を Formula に含めることで、プライベートリポジトリの Releases にあるファイルを Personal Access Token を使って GitHub API 経由でダウンロードすることができるようになります。

まとめ
  • 配信側は Formula を用意し、それを入れた tap を用意する必要があります。

    • Formula 内では OS や CPU に合わせてダウンロードする URL を切り替えることができます。

    • Homebrew がプライベートリポジトリからファイルをダウンロードできるように、CurlDownloadStrategy を拡張した Strategy を定義し、Formula から使います。

  • ユーザー側は Personal Access Token を Homebrew にセットし、tap を追加する必要があります。

ここまでやると、

brew install my-cool/tap/my-cool-tool

といったようにコマンド一発でツールをインストールできるようになります。

最後に

estie では、全社横断の技術的な課題を解決するために、自前のツールを開発・配布することがあります。社内用の tap はすでにありますので、ガンガン開発してくださる方をお待ちしています。

hrmos.co

© 2019- estie, inc.