#!/bin/sh # Install the aikagi CLI. # # curl -fsSL https://aikagi.dev/install.sh | sh # # Environment: # AIKAGI_VERSION version to install (default: whatever latest.txt names) # AIKAGI_INSTALL_DIR where to put the binary (default: $HOME/.local/bin) # AIKAGI_DOWNLOAD_BASE where to fetch from (testing only) # AIKAGI_UNAME_S override the detected OS (testing only) # AIKAGI_UNAME_M override the detected architecture (testing only) # # Everything lives inside a function that is called on the last line. A truncated # download therefore does nothing at all, rather than half-installing: `sh` only # starts running once it has read the call, and it cannot read the call until it # has read the whole file. # # The archive's checksum is always verified. There is no flag to skip it. # When `minisign` is installed, the release signature is verified too, against the # public keys below (the same ones the CLI carries; crates/aikagi/release-keys.pub). set -eu # minisign public keys that sign releases. At most two, during a key rotation. # scripts/release-signing.test.mjs keeps this line equal to crates/aikagi/release-keys.pub. RELEASE_KEYS="RWSx1GnS83W82KZkWo8NSIepeqDIwSfe08rgF/D9XwaXeeI7k4mV9zNg" # Versions published before releases were signed. Anything else must carry a signature. UNSIGNED_RELEASES="v0.1.0 v0.1.1 v0.2.0 v0.2.1 v0.2.2 v0.3.0 v0.3.1" aikagi_install() { base="${AIKAGI_DOWNLOAD_BASE:-https://dl.aikagi.dev}" DRY_RUN=0 [ "${1:-}" = "--dry-run" ] && DRY_RUN=1 die() { echo "install.sh: $*" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } # 版の名前はそのまま URL の一部になる。`latest.txt` の中身も AIKAGI_VERSION も # 配布元と手元が名乗ったままの文字列なので、`../` のような綴りを埋める前に形を # 確かめる。CLI 側の `valid_tag`(crates/aikagi/src/update.rs)と同じ形。 valid_version() { printf '%s' "$1" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.]+)?$' } os="${AIKAGI_UNAME_S:-$(uname -s)}" arch="${AIKAGI_UNAME_M:-$(uname -m)}" # 案内してよい組み合わせは dist-targets.json が決める(CI の # scripts/check-dist-targets.mjs がこの分岐と突き合わせる)。まだ書庫を # 出していない組み合わせは、取りに行かずにここで断る。 case "$os" in Darwin) die "there is no published macOS build yet" ;; Linux) case "$arch" in x86_64 | amd64) target="x86_64-unknown-linux-musl" ;; aarch64 | arm64) target="aarch64-unknown-linux-musl" ;; *) die "unsupported Linux architecture: $arch" ;; esac ;; MINGW* | MSYS* | CYGWIN* | Windows_NT) # Windows 上の sh から流されたとき。Windows 版は install.ps1 が入れる。 die "unsupported platform: $os. On Windows, run https://aikagi.dev/install.ps1 in PowerShell, or install the Linux build inside WSL2" ;; *) die "unsupported platform: $os" ;; esac archive="aikagi-${target}.tar.gz" version="${AIKAGI_VERSION:-}" if [ -n "$version" ]; then valid_version "$version" \ || die "AIKAGI_VERSION is not a version name: ${version}; write it as vX.Y.Z" fi if [ -z "$version" ] && [ "$DRY_RUN" -eq 0 ]; then have curl || die "curl is required" # latest.txt は最新の版の名前が 1 行だけ書かれた平文。余計な空白と改行は落とす。 version=$(curl -fsSL "${base}/latest.txt" | head -n1 | tr -d ' \t\r') [ -n "$version" ] \ || die "cannot determine the latest version; set AIKAGI_VERSION=vX.Y.Z" # 名乗られた綴りをそのまま URL に入れない。取りに行く前に断る。 valid_version "$version" \ || die "the download site named ${version}, which is not a version; set AIKAGI_VERSION=vX.Y.Z" fi # 配布元の版のディレクトリは v 付き(`v0.4.0/`)。`0.4.0` と書かれても同じ版を取りに行く。 # 署名の trusted comment も v 付きの綴りで照らす。 case "$version" in "" | v*) ;; *) version="v$version" ;; esac # --dry-run で版を聞かれていないとき。実在するパスではないので、 # 貼って使えない綴りだと分かる形で見せる。 [ -n "$version" ] || version="" url="${base}/${version}/${archive}" if [ "$DRY_RUN" -eq 1 ]; then echo "$url" return 0 fi have curl || die "curl is required" have tar || die "tar is required" if have sha256sum; then sha_cmd="sha256sum" elif have shasum; then sha_cmd="shasum -a 256" else die "no sha256 tool found (install coreutils or perl); refusing to install unverified" fi install_dir="${AIKAGI_INSTALL_DIR:-$HOME/.local/bin}" tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT INT TERM echo "install.sh: downloading $url" >&2 # 404 は「この版にこのプラットフォーム向けの書庫が無い」。ネットワーク障害と # 混ぜずに、その場で次の手を案内する。 # `-f` は付けない。付けると 404 でも exit だけが返り、どの失敗なのか分からない。 code=$(curl -sSL -w '%{http_code}' "$url" -o "$tmp/$archive" 2>/dev/null || echo 000) case "$code" in 2*) ;; 404) # 404 は 2 通りある。その版は在るがこのプラットフォーム向けの書庫が無い場合と、 # その版自体が無い場合。案内が変わるので、同じ版の別の書庫を 1 つ当たって確かめる。 rel=$(curl -sSL -o /dev/null -w '%{http_code}' \ "${base}/${version}/aikagi-x86_64-unknown-linux-musl.tar.gz.sha256" 2>/dev/null || echo 000) if [ "$rel" = "200" ]; then die "version ${version} has no build for ${target}" fi die "cannot see version ${version} (wrong version name, or it has not been published); check AIKAGI_VERSION" ;; *) die "download failed ($code): $url" ;; esac [ -s "$tmp/$archive" ] || die "download failed: $url" # 書庫の隣に必ずチェックサムを置いてある。無ければ入れない: このスクリプトは # パイプから無人で走るので、危険かどうかを判断する人がその場に居ない。 curl -fsSL "${url}.sha256" -o "$tmp/$archive.sha256" \ || die "no published checksum for $archive; refusing to install" expected=$(cut -d' ' -f1 < "$tmp/$archive.sha256") [ -n "$expected" ] || die "the published checksum for $archive is empty" actual=$($sha_cmd "$tmp/$archive" | cut -d' ' -f1) [ "$actual" = "$expected" ] || die "checksum mismatch for $archive" # 署名。チェックサムは書庫と同じ場所に並んでいるので、そこに書ける人なら揃えて # 差し替えられる。署名の鍵は配布元には無い。minisign があれば必ず確かめる。 signed_era=1 for old in $UNSIGNED_RELEASES; do [ "$version" = "$old" ] && signed_era=0; done if [ -n "$RELEASE_KEYS" ] && have minisign; then if curl -fsSL "${url}.minisig" -o "$tmp/$archive.minisig" 2>/dev/null; then ok=0 for key in $RELEASE_KEYS; do if minisign -Vqm "$tmp/$archive" -x "$tmp/$archive.minisig" -P "$key" >/dev/null 2>&1; then ok=1 fi done [ "$ok" -eq 1 ] || die "the signature of $archive does not verify; refusing to install" # trusted comment は署名が覆っている。別の版・別の書庫の署名の流用を断る。 [ "$(sed -n 3p "$tmp/$archive.minisig")" = "trusted comment: file:$archive version:$version" ] \ || die "the signature belongs to a different file; refusing to install" echo "install.sh: verified the checksum and the release signature" >&2 elif [ "$signed_era" -eq 1 ]; then die "no published signature for $archive; refusing to install" else echo "install.sh: $version was published before releases were signed; verified its checksum only" >&2 fi else echo "install.sh: verified the checksum; install minisign to also verify the release signature" >&2 fi tar -xzf "$tmp/$archive" -C "$tmp" bin=$(find "$tmp" -type f -name aikagi -perm -u+x | head -n1) [ -n "$bin" ] || die "the archive did not contain an aikagi binary" # No sudo anywhere: everything goes under the user's own home. mkdir -p "$install_dir" install -m 0755 "$bin" "$install_dir/aikagi" 2> /dev/null || { cp "$bin" "$install_dir/aikagi" && chmod 0755 "$install_dir/aikagi" } echo "install.sh: installed $("$install_dir/aikagi" --version) to $install_dir/aikagi" >&2 case ":$PATH:" in *":$install_dir:"*) ;; *) echo "install.sh: add $install_dir to your PATH" >&2 ;; esac echo "install.sh: next, run: aikagi login" >&2 } aikagi_install "${1:-}"