| 1 | #!/bin/sh |
| 2 | set -eu |
| 3 | |
| 4 | repo="Hmbown/CodeWhale" |
| 5 | version="${CODEWHALE_VERSION:-latest}" |
| 6 | release_base="${CODEWHALE_RELEASE_BASE_URL:-${DEEPSEEK_TUI_RELEASE_BASE_URL:-}}" |
| 7 | |
| 8 | usage() { |
| 9 | cat <<'USAGE' |
| 10 | Codewhale installer for macOS and Linux. |
| 11 | |
| 12 | Usage: |
| 13 | curl -fsSL https://codewhale.net/install.sh | sh |
| 14 | |
| 15 | Environment: |
| 16 | CODEWHALE_INSTALL_DIR Install directory. Default: $HOME/.local/bin |
| 17 | CODEWHALE_VERSION Release tag to install, for example v0.9.0. Default: latest |
| 18 | CODEWHALE_RELEASE_BASE_URL |
| 19 | Custom release asset base URL ending in /download |
| 20 | CODEWHALE_SKIP_GLIBC_CHECK=1 |
| 21 | Skip Linux arm64 glibc compatibility preflight |
| 22 | |
| 23 | Examples: |
| 24 | curl -fsSL https://codewhale.net/install.sh | CODEWHALE_INSTALL_DIR=/usr/local/bin sh |
| 25 | curl -fsSL https://codewhale.net/install.sh | CODEWHALE_VERSION=v0.9.0 sh |
| 26 | USAGE |
| 27 | } |
| 28 | |
| 29 | case "${1:-}" in |
| 30 | -h|--help) |
| 31 | usage |
| 32 | exit 0 |
| 33 | ;; |
| 34 | esac |
| 35 | |
| 36 | say() { |
| 37 | printf '%s\n' "$*" |
| 38 | } |
| 39 | |
| 40 | fail() { |
| 41 | printf 'codewhale install: %s\n' "$*" >&2 |
| 42 | exit 1 |
| 43 | } |
| 44 | |
| 45 | if [ -n "${CODEWHALE_INSTALL_DIR:-}" ]; then |
| 46 | install_dir="$CODEWHALE_INSTALL_DIR" |
| 47 | else |
| 48 | [ -n "${HOME:-}" ] || fail "HOME is not set; set CODEWHALE_INSTALL_DIR" |
| 49 | install_dir="$HOME/.local/bin" |
| 50 | fi |
| 51 | |
| 52 | need_cmd() { |
| 53 | command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1" |
| 54 | } |
| 55 | |
| 56 | download() { |
| 57 | url="$1" |
| 58 | out="$2" |
| 59 | if command -v curl >/dev/null 2>&1; then |
| 60 | curl -fsSL "$url" -o "$out" |
| 61 | elif command -v wget >/dev/null 2>&1; then |
| 62 | wget -q "$url" -O "$out" |
| 63 | else |
| 64 | fail "curl or wget is required" |
| 65 | fi |
| 66 | } |
| 67 | |
| 68 | sha256_file() { |
| 69 | file="$1" |
| 70 | if command -v sha256sum >/dev/null 2>&1; then |
| 71 | sha256sum "$file" | awk '{print $1}' |
| 72 | elif command -v shasum >/dev/null 2>&1; then |
| 73 | shasum -a 256 "$file" | awk '{print $1}' |
| 74 | else |
| 75 | fail "sha256sum or shasum is required to verify downloads" |
| 76 | fi |
| 77 | } |
| 78 | |
| 79 | verify_asset() { |
| 80 | asset="$1" |
| 81 | file="$2" |
| 82 | manifest="$3" |
| 83 | expected="$( |
| 84 | awk -v name="$asset" ' |
| 85 | { |
| 86 | digest = tolower($1) |
| 87 | file = $2 |
| 88 | sub(/^\*/, "", file) |
| 89 | if (file == name && digest ~ /^[0-9a-f]{64}$/) { |
| 90 | print digest |
| 91 | exit |
| 92 | } |
| 93 | } |
| 94 | ' "$manifest" |
| 95 | )" |
| 96 | [ -n "$expected" ] || fail "checksum not found for $asset" |
| 97 | actual="$(sha256_file "$file" | tr '[:upper:]' '[:lower:]')" |
| 98 | [ "$actual" = "$expected" ] || fail "checksum mismatch for $asset" |
| 99 | } |
| 100 | |
| 101 | glibc_version() { |
| 102 | if command -v getconf >/dev/null 2>&1; then |
| 103 | getconf GNU_LIBC_VERSION 2>/dev/null | awk '{ print $NF; exit }' |
| 104 | return |
| 105 | fi |
| 106 | if command -v ldd >/dev/null 2>&1; then |
| 107 | ldd --version 2>/dev/null | awk 'NR == 1 { |
| 108 | for (i = 1; i <= NF; i++) { |
| 109 | if ($i ~ /^[0-9]+\.[0-9]+/) { |
| 110 | print $i |
| 111 | exit |
| 112 | } |
| 113 | } |
| 114 | }' |
| 115 | fi |
| 116 | } |
| 117 | |
| 118 | version_at_least() { |
| 119 | have="$1" |
| 120 | need="$2" |
| 121 | awk -v have="$have" -v need="$need" ' |
| 122 | BEGIN { |
| 123 | split(have, h, ".") |
| 124 | split(need, n, ".") |
| 125 | for (i = 1; i <= 3; i++) { |
| 126 | hv = h[i] + 0 |
| 127 | nv = n[i] + 0 |
| 128 | if (hv > nv) exit 0 |
| 129 | if (hv < nv) exit 1 |
| 130 | } |
| 131 | exit 0 |
| 132 | } |
| 133 | ' |
| 134 | } |
| 135 | |
| 136 | check_glibc() { |
| 137 | case "$target" in |
| 138 | linux-arm64) ;; |
| 139 | *) return ;; |
| 140 | esac |
| 141 | |
| 142 | [ "${CODEWHALE_SKIP_GLIBC_CHECK:-}" = "1" ] && return |
| 143 | [ "${DEEPSEEK_TUI_SKIP_GLIBC_CHECK:-}" = "1" ] && return |
| 144 | [ "${DEEPSEEK_SKIP_GLIBC_CHECK:-}" = "1" ] && return |
| 145 | |
| 146 | required="2.39" |
| 147 | host="$(glibc_version || true)" |
| 148 | if [ -z "$host" ] || ! version_at_least "$host" "$required"; then |
| 149 | cat >&2 <<EOF |
| 150 | codewhale install: prebuilt Codewhale $target assets require glibc $required or newer. |
| 151 | This system reports glibc ${host:-unavailable}. |
| 152 | |
| 153 | Linux x64 uses a static musl build. Linux arm64 release assets are GNU libc |
| 154 | builds from Ubuntu 24.04. Build from source with Cargo or set |
| 155 | CODEWHALE_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk. |
| 156 | EOF |
| 157 | exit 1 |
| 158 | fi |
| 159 | } |
| 160 | |
| 161 | detect_platform() { |
| 162 | os="$(uname -s)" |
| 163 | arch="$(uname -m)" |
| 164 | |
| 165 | case "$os" in |
| 166 | Darwin) platform="macos" ;; |
| 167 | Linux) platform="linux" ;; |
| 168 | *) fail "unsupported OS: $os. Use npm, Cargo, or the GitHub Releases page." ;; |
| 169 | esac |
| 170 | |
| 171 | case "$arch" in |
| 172 | x86_64|amd64) cpu="x64" ;; |
| 173 | arm64|aarch64) cpu="arm64" ;; |
| 174 | riscv64) fail "Linux riscv64 prebuilt assets are temporarily unavailable because the locked rquickjs-sys dependency does not ship riscv64gc bindings." ;; |
| 175 | *) fail "unsupported CPU architecture: $arch. Use Cargo or build from source." ;; |
| 176 | esac |
| 177 | |
| 178 | printf '%s-%s' "$platform" "$cpu" |
| 179 | } |
| 180 | |
| 181 | if [ -z "$release_base" ]; then |
| 182 | if [ "$version" = "latest" ]; then |
| 183 | release_base="https://github.com/$repo/releases/latest/download" |
| 184 | else |
| 185 | release_base="https://github.com/$repo/releases/download/$version" |
| 186 | fi |
| 187 | fi |
| 188 | |
| 189 | target="$(detect_platform)" |
| 190 | check_glibc |
| 191 | cli_asset="codewhale-$target" |
| 192 | tui_asset="codewhale-tui-$target" |
| 193 | manifest_asset="codewhale-artifacts-sha256.txt" |
| 194 | |
| 195 | tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t codewhale-install)" |
| 196 | trap 'rm -rf "$tmpdir"' EXIT INT TERM |
| 197 | |
| 198 | say "Installing Codewhale for $target" |
| 199 | say "Release assets: $release_base" |
| 200 | say "Install dir: $install_dir" |
| 201 | |
| 202 | download "$release_base/$manifest_asset" "$tmpdir/$manifest_asset" |
| 203 | download "$release_base/$cli_asset" "$tmpdir/codewhale" |
| 204 | download "$release_base/$tui_asset" "$tmpdir/codewhale-tui" |
| 205 | |
| 206 | verify_asset "$cli_asset" "$tmpdir/codewhale" "$tmpdir/$manifest_asset" |
| 207 | verify_asset "$tui_asset" "$tmpdir/codewhale-tui" "$tmpdir/$manifest_asset" |
| 208 | say "Checksums verified" |
| 209 | |
| 210 | chmod 755 "$tmpdir/codewhale" "$tmpdir/codewhale-tui" |
| 211 | if command -v xattr >/dev/null 2>&1; then |
| 212 | xattr -d com.apple.quarantine "$tmpdir/codewhale" "$tmpdir/codewhale-tui" 2>/dev/null || true |
| 213 | fi |
| 214 | |
| 215 | sudo_cmd="" |
| 216 | if [ -d "$install_dir" ]; then |
| 217 | if [ ! -w "$install_dir" ] || |
| 218 | { [ -e "$install_dir/codewhale" ] && [ ! -w "$install_dir/codewhale" ]; } || |
| 219 | { [ -e "$install_dir/codewhale-tui" ] && [ ! -w "$install_dir/codewhale-tui" ]; } || |
| 220 | { [ -e "$install_dir/codew" ] && [ ! -w "$install_dir/codew" ]; }; then |
| 221 | need_cmd sudo |
| 222 | sudo_cmd="sudo" |
| 223 | fi |
| 224 | else |
| 225 | if ! mkdir -p "$install_dir" 2>/dev/null; then |
| 226 | need_cmd sudo |
| 227 | sudo mkdir -p "$install_dir" |
| 228 | sudo_cmd="sudo" |
| 229 | fi |
| 230 | fi |
| 231 | |
| 232 | stage_cli="$install_dir/.codewhale.$$" |
| 233 | stage_tui="$install_dir/.codewhale-tui.$$" |
| 234 | trap 'rm -rf "$tmpdir"; rm -f "$stage_cli" "$stage_tui" 2>/dev/null || true' EXIT INT TERM |
| 235 | |
| 236 | $sudo_cmd cp "$tmpdir/codewhale" "$stage_cli" |
| 237 | $sudo_cmd cp "$tmpdir/codewhale-tui" "$stage_tui" |
| 238 | $sudo_cmd chmod 755 "$stage_cli" "$stage_tui" |
| 239 | $sudo_cmd mv "$stage_cli" "$install_dir/codewhale" |
| 240 | $sudo_cmd mv "$stage_tui" "$install_dir/codewhale-tui" |
| 241 | |
| 242 | $sudo_cmd rm -f "$install_dir/codew" |
| 243 | if ! $sudo_cmd ln -s codewhale "$install_dir/codew"; then |
| 244 | say "Installed binaries, but could not create $install_dir/codew alias" |
| 245 | fi |
| 246 | |
| 247 | say "Installed:" |
| 248 | "$install_dir/codewhale" --version || true |
| 249 | "$install_dir/codewhale-tui" --version || true |
| 250 | |
| 251 | case ":$PATH:" in |
| 252 | *":$install_dir:"*) ;; |
| 253 | *) |
| 254 | say "" |
| 255 | say "Add $install_dir to PATH to run codewhale from any terminal." |
| 256 | ;; |
| 257 | esac |
| 258 | |
| 259 | say "" |
| 260 | say "Run: codewhale" |
| 261 |