GitHubのリポジトリーにインストーラーを置きたくて、調べたけどカンタンにパクれる参考にできる良い方法が見つからなかったので、スクラッチで作ってみました。
せっかくなので公開しておきます。
追記(2017-04-30): 大幅に修正しました。
はじめに
パッケージ管理に入れるほどではないアプリ――野良アプリ?――を作った場合、カンタンにインストールできるようにしておけば、すぐに試してもらうことができるようになります。
要は、README.md
にこう書きたいんです。
インストールするには、以下のコマンドを実行してください。
$ curl -fsSL https://raw.githubusercontent.com/... | sh
前提条件
ビルド済みバイナリーを配布している前提です。
今回紹介する例は、Javaで書いたJARファイルで実行するタイプのアプリをZIPで配布していますが、他のタイプでもビルド済みバイナリーであれば応用はできると思います。
配布物件のファイル名は、バージョンが1.0.0-beta1-SNAPSHOT
の場合、myapp1-1.0.0-beta1-SNAPSHOT-bin.zip
の中にmyapp1-1.0.0-beta1-SNAPSHOT.jar
を格納するようにします。ZIPファイルには、他の配布物件も含まれています。
実行環境
OS・処理系など
動作を確認した環境は、以下のとおりです。
(すべて64bit)
- Ubuntu 14.04
- CentOS 7 1611
- Fedora 25
- Darwin 16.5.0 (macOS Sierra 10.12.4)
- CYGWIN_NT-10.0 on Windows 10
また、動作確認はしていませんが、Debian 8.6.0を情報採取に使用しました。
使用しているモジュール・ツール
cURL
, unzip
と、標準コマンド以外は使わないで実行できるようにしました。
実装
先に完成版を載せておきます。
argius
のmyapp1
リポジトリー(実際には存在しない)にある、1.0.0-beta1-SNAPSHOT
をインストールするスクリプトです。
install.sh
#!/bin/sh
# Installer
set -eu
prodname=myapp1
ver=1.0.0-beta1
owner=argius
execname=myapp1
execdir=/usr/local/bin
dir=~/.$prodname
baseurl=https://github.com/$owner/$prodname
bindir=$dir/bin
libdir=$dir/lib
zipfile=${prodname}-${ver}-bin.zip
zipurl=$baseurl/releases/download/v$ver/$zipfile
jarfile=${prodname}-${ver}.jar
jarpath=$libdir/$jarfile
binfile=$bindir/$execname
execfile=$execdir/$execname
errexit() {
printf "\033[31m[ERROR]\033[0m" ; echo " $1"
echo "Installation incomplete."
exit 1
}
onexit() {
if [ -n "$tmpdir" ]; then
cd $tmpdir/..
test -f $tmpdir/$jarfile && rm $tmpdir/$jarfile
test -f $tmpdir/$zipfile && rm $tmpdir/$zipfile
rmdir $tmpdir
fi
}
tmpdir=`mktemp -d /tmp/${prodname}-XXXXXX`
trap onexit EXIT
trap "trap - EXIT; onexit; exit -1" 1 2 15 # SIGHUP SIGINT SIGTERM
echo "This is the installer of \"$prodname\"."
echo ""
# OS specific settings
case "`uname -a`" in
Linux* )
echo "adjusting for Linux"
if [ -d ~/.local/bin ] && [ -w ~/.local/bin ]; then
execdir=~/.local/bin
elif [ -d ~/bin ] && [ -w ~/bin ]; then
execdir=~/bin
elif [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then
execdir=/usr/local/bin
else
errexit "cannot detect writable exec dir, requires ~/.local/bin or ~/bin"
fi
execfile=$execdir/$execname
echo ""
;;
*BSD* )
echo "adjusting for *BSD"
if [ -d ~/bin ] && [ -w ~/bin ]; then
execdir=~/bin
elif [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then
execdir=/usr/local/bin
else
errexit "cannot detect writable exec dir, requires ~/bin"
fi
execfile=$execdir/$execname
echo ""
;;
CYGWIN* )
echo "adjusting for Cygwin"
jarpath=`cygpath -m $jarpath`
echo ""
;;
esac
echo "installing version $ver into $execdir and $dir,"
echo "and uses $tmpdir as a working directory."
cd $tmpdir || errexit "failed to change directory"
echo "downloading: $zipurl"
curl -fsSLO $zipurl || errexit "failed to download zip"
unzip -o $zipfile $jarfile || errexit "failed to unzip"
mkdir -p $libdir && cp -fp $jarfile $libdir/
test -f $libdir/$jarfile || errexit "failed to copy jar file"
mkdir -p $bindir && ( echo "#!/bin/sh" ; echo "java -jar $jarpath \$@" ) > $binfile
test -f $binfile || errexit "failed to create $binfile"
chmod +x $binfile || errexit "failed to change a permission"
ln -sf $binfile $execfile || errexit "failed to create a symlink of $binfile"
echo "\"$prodname\" has been installed to $execdir and $dir/ ."
echo "checking installation => `$execname --version`"
echo ""
echo "Installation completed."
以下はCentOSでの実行例です。
- 実行イメージ
$ sh install.sh
This is the installer of "myapp1".
adjusting for Linux
[ERROR] cannot detect writable exec dir, requires ~/.local/bin or ~/bin
Installation incomplete.
$ mkdir ~/.local/bin
$ sh install.sh
This is the installer of "myapp1".
adjusting for Linux
installing version 1.0.0-beta1 into /home/argius/.local/bin and /home/argius/.myapp1,
and uses /tmp/myapp1-0XGFEP as a working directory.
downloading: https://github.com/argius/myapp1/releases/download/v1.0.0-beta1/myapp1-1.0.0-beta1-bin.zip
Archive: myapp1-1.0.0-beta1-bin.zip
inflating: myapp1-1.0.0-beta1.jar
"myapp1" has been installed to /home/argius/.local/bin and /home/argius/.myapp1/ .
checking installation => Myapp1 version 1.0.0-beta1
Installation completed.
$ myapp1
Hello argius, this is myapp1.
$
解説
このシェルスクリプトで使用している基本的なテクニックと、このスクリプトを書く上で考えたことについて補足します。
OSごとの対応
どのディレクトリーにインストールすべきかを判断するため、いくつかのOSで情報採取しました。
列の意味は、以下の通りです。
-
uname
でディストリビューションなどの判定が可能か -
/usr/local/bin
への書き込みが可能か -
~/.local
ディレクトリーなどが初期状態で存在するか -
~/.local/bin
などに初期状態でパスが通っているか
uname 個別 |
/usr/local/bin 書き込み |
~/.local 有 |
~/.local/bin 有 |
~/bin 有 |
~/.local/bin パス |
~/bin パス |
|
---|---|---|---|---|---|---|---|
Ubuntu | ○ | × | × | × | × | × | ○ |
CentOS | × | × | ○ | × | × | ○ | ○ |
Fedora | × | × | ○ | × | × | ○ | ○ |
Debian | ○ | × | × | × | × | × | × |
Free BSD | ○ | × | × | × | × | × | ○ |
Mac | ○ | ○ | - | - | - | - | - |
Cygwin | ○ | ○ | - | - | - | - | - |
Linuxは個別判定が難しいと判断し、多数決で~/.local/bin
と~/bin
をインストール先候補にしています。(これについては、コメント欄も参照して下さい。)
BSD系はサンプルが少なく、私も文化を良く知らないので、FreeBSDを代表として、~/bin
をインストール先候補にしています。
MacやCygwinは/usr/local/bin
に書き込み可と判断し、そこにインストールすることにしました。
上記以外のOSについては、/usr/local/bin
にインストールするものとします。
Cygwinについては、CygwinからWindows版のJavaを使用している前提です。CygwinのパスをそのままJavaに渡すと上手く行きませんので、cygpath
で変換したパスを渡すようにしています。
一時ディレクトリーの処理
Thanks to @tenmyo さん
下記の記事を参考にしました。
一時ディレクトリーの作成は、mktemp -d
を使用しています。
エラーや強制終了などで中断した場合でも削除できるように、trap
を使用します。
ファイルとディレクトリーの削除は、意地でもワイルドカードを使わないようにしています。
ただ、この例ではファイルを2つしか展開しないのでこれで済んでいますが、展開されるファイルの数が増えるとワイルドカードを使わないのは難しいでしょうね。
シグナル番号を定数を使わずに数値リテラルで指定しているのは、一部の環境の/bin/sh
では定数を使用できないためです。
set -eu
する
-e
は、エラーが発生したら処理を中断するオプションです。
-u
は、宣言されていない変数を使おうとした場合にエラーにしてくれるオプションです。
今回のスクリプトではエラー処理を細かく入れているのであまり意味はないですが、環境によっては予期しないエラーが起きる可能性はありますので、付けておくと少し安心です。
エラー処理
errexit
関数を定義しておき、コマンドでエラーを返されたら終了できるようにしておきます。
典型的な使い方は以下のようなものです。
test -f /path/to/target || errexit "ファイルが無い"
||
はOR条件を表します。test
が成功していれば||
以降は無視されます。test
が失敗した場合は、||
以降が実行され、メッセージを出力した上でスクリプトを終了させています。
成功かどうかの判定は、コマンド実行直後の特殊変数$?
の値がゼロなら成功、それ以外なら失敗と見做されます。
echo
の引用符
echo
は引用符が無くても動作しますが、スクリプトの見通しが良くなるので基本的につけるようにしています。
進捗メッセージ
無言だと、何をしているのかとか、上手く行っているのかとか、ユーザーさんを不安にさせてしまいますので、適度な量のメッセージを出力します。
先頭と末尾に空行を設けているのは、開始と終了を分かりやすくしているつもりです。
~/.myapp1
を使う
~/.myapp1
には他にも必要なファイルを保管したりするのに使うので、/usr/local/bin
に置くスクリプトファイル以外はここにまとめます。
実行スクリプトをその場で作成
実行スクリプトをリポジトリーで管理するとバージョンアップの際などに更新する手間が増えてしまうので、インストーラーに作らせています。
動作することを確認する
インストールが上手く行ったかどうかを、実際に実行して確認します。
myapp1
は--version
オプションでバージョンを出力するようになっていますので、そのコマンドを実行します。
英語のメッセージについて
それっぽいものを使っていますが、あまり自信がありませんので、ご利用の際にはその点にご留意願います。
インストーラーの設置
インストーラーをリポジトリーのmasterブランチに登録しておきます。
先述の例に従うと、URLはhttps://raw.githubusercontent.com/argius/myapp1/master/install.sh
になります。https://raw.githubusercontent.com/
経由でアクセスすると、HTMLで修飾されていない生のファイルを参照することができます。これは、ファイルのページにある"Raw"ボタンを選択したときのURLと同じです。
このままでも良いですが、ちょっと長いので、短縮URLでインストーラーのURLを短縮します。
ここではGoogle URL Shortenerを使いました。
実際には作っていないので、以降はダミーのURLhttps://goo.gl/0v8bcj
(google.com
の短縮)を使います。
※本物ではないので試さないでください。
$ curl -fsSL https://goo.gl/0v8bcj | sh
This is the installer of "myapp1".
(snip)
Installation completed.
$
できました!
おわりに
作ってはみたものの、これで良いんだろうかと思わないでもないです。
もっと汎用的な方法があったら教えてください。