HerokuとGithubを使った統一的なツール配布
Go言語ではクロスコンパイルがとても簡単で,複数プラットフォーム向けのバイナリをつくってそれを配布するというのがさらっとできる.
単純にやるなら,
そして,ユーザには自分のプラットフォームに合ったものをダウンロード/展開してPATHの通ったところに置いてもらう.
開発者からすると,すごい簡単.ホストするまで完全に自動化できる.でも,ユーザからすると若干めんどくさい.
もっとツールを使い初めてもらうまでの敷居を下げたい.
TL;DR
全プラットフォーム共通で以下のようにツールをインストールできるようにする.若干長いが1コマンド!
$ L=/usr/local/bin/ghr && curl -sL -A "`uname -sp`" http://ghr.herokuapp.com/ghr.zip | zcat >$L && chmod +x $L
このような配布をHerokuとGithubを使ってできるようにする.
実例
このようなツール配布を行っている例はいくつかある.
例えば,Herokuのhkは,以下のようにインストールできる.
$ L=/usr/local/bin/hk && curl -sL -A "`uname -sp`" https://hk.heroku.com/hk.gz | zcat >$L && chmod +x $L
動作の概要
Githubにリリースを作成し,各プラットフォーム向けのパッケージがホストされているとする.
動作の流れは以下のようになる.
- ユーザがHerokuアプリに対してリクエストを投げる
- アプリはリクエストに基づきプラットフォームを判定し,それに合ったGithub Release上のパッケージへのリダイレクトを返す
- ユーザはプラットフォームに合ったパッケージをGithub Releaseから得る
具体的な動作
Githubリリースの作り方,ワンライナーの動作,Herokuアプリについて簡単に説明する.
Github Release
まず,Github Releaseページに作成したパッケージをホストしておく.パッケージ名は以下のルールに従うようにする.
${NAME}_${VERSION}_${OS}_${ARCH}.zip
NAME
はツール名,OS
はプラットフォーム名,ARCH
はプロセッサを指定する.すると,Github Release上のダウンロードURLは以下のようになる.
https://github.com/tcnksm/${NAME}/releases/download/${VERSION}/${NAME}_${VERSION}_${OS}_${ARCH}.zip
これは自動化できる(“高速に自作パッケージをGithubにリリースするghrというツールをつくった”).
ワンライナー
ワンライナーでやっているのは,
- 環境変数
L
にインストールしたいPATHを指定する curl
では,-L
オプションでリクエストで30X
の場合にリダイレクトするようにし,-A
でユーザエージェントを指定するzcat
でzipを展開してL
に吐き出す
ユーザエージェントはuname
で指定する.-s
オプションでシステム名(e.g., Darwin,Linux)を,-p
オプションでプロセッサ(e.g., i386)を出力するようにする.
Herokuアプリ
curl
のアクセス先は,専用に立てたHerokuアプリになる.Heorkuアプリでは,ユーザエージェントを元にプラットフォームを判別し,Github Releaseページ上のパッケージにリダイレクトさせる.
アプリはGo言語で書いた.以下はその抜粋.
func main() {
http.HandleFunc("/"+os.Getenv("NAME")+".zip", binary)
}
func binary(w http.ResponseWriter, r *http.Request) {
platform := guessPlatform(r.UserAgent())
http.Redirect(w, r, binaryURL(platform), http.StatusTemporaryRedirect)
}
func binaryURL(platform string) string {
return os.Getenv("BASE_URL") + "/download/" + os.Getenv("VERSION") + "/" + os.Getenv("NAME") + "_" + os.Getenv("VERSION") + "_" + platform + ".zip"
}
func guessOS(userAgent string) string {
if isDarwin(userAgent) {
return "darwin"
}
if isWindows(userAgent) {
return "windows"
}
return "linux"
}
func guessArch(userAgent string) string {
if isAmd64(userAgent) || isDarwin(userAgent) {
return "amd64"
}
return "386"
}
func guessPlatform(userAgent string) string {
userAgent = strings.ToLower(userAgent)
return guessOS(userAgent) + "_" + guessArch(userAgent)
}
func isDarwin(userAgent string) bool {
return strings.Contains(userAgent, "mac os x") || strings.Contains(userAgent, "darwin")
}
func isWindows(userAgent string) bool {
return strings.Contains(userAgent, "windows")
}
func isAmd64(userAgent string) bool {
return strings.Contains(userAgent, "x86_64") || strings.Contains(userAgent, "amd64")
}
Github ReleaseページへのリダイレクトURLは,バージョン名($VERSION
)やツール名($NAME
)を環境変数で指定しておき,それから組み立てるようにしている.
運用
新しいバージョンをリリースしたら,Herokuアプリの$VERSION
環境変数を更新するだけなのでとても楽.
さらに言えば,HerokuアプリのセットアップはTerraformを使っているので(“TerraformでHerokuアプリのセットアップ”),設定ファイル書き換えてterraform apply
するだけ.自動化できそう.
Github Release上のパッケージ名が上記のルールに従っていれば,誰でも環境変数を変えるだけで使える.興味があればForkで何でもいいので使ってください.
他のやりかた
もちろんOSXに向けてHomebrewレシピ,Debina系に向けてdebパッケージ,Red Hat系に向けてRPMパッケージがそれぞれ準備されているのが理想的.でも,個人開発だとそれを作る,メンテしていくコストが高い.
ただHomebrewのレシピは,クソ簡単なので準備してあげるとよい(“HomeBrewで自作ツールを配布する”).