返回 CodeWhale
install.sh
根目录 / web / public / install.sh
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 GitHub release installer for new macOS and Linux installations.
11 For an existing direct install, run its codewhale update command.
12
13 Usage:
14 curl -fsSL https://codewhale.net/install.sh | sh
15
16 Environment:
17 CODEWHALE_INSTALL_DIR Install directory. Default: $HOME/.local/bin
18 CODEWHALE_VERSION Release tag for a fresh directory. Default: latest
19 CODEWHALE_RELEASE_BASE_URL
20 Custom release asset base URL ending in /download
21 CODEWHALE_SKIP_GLIBC_CHECK=1
22 Skip Linux arm64 glibc compatibility preflight
23
24 Examples:
25 curl -fsSL https://codewhale.net/install.sh | CODEWHALE_INSTALL_DIR="$HOME/.local/codewhale/bin" sh
26 curl -fsSL https://codewhale.net/install.sh | CODEWHALE_VERSION=vX.Y.Z sh
27 USAGE
28 }
29
30 case "${1:-}" in
31 -h|--help)
32 usage
33 exit 0
34 ;;
35 esac
36
37 say() {
38 printf '%s\n' "$*"
39 }
40
41 fail() {
42 printf 'codewhale install: %s\n' "$*" >&2
43 exit 1
44 }
45
46 if [ -n "${CODEWHALE_INSTALL_DIR:-}" ]; then
47 install_dir="$CODEWHALE_INSTALL_DIR"
48 else
49 [ -n "${HOME:-}" ] || fail "HOME is not set; set CODEWHALE_INSTALL_DIR"
50 install_dir="$HOME/.local/bin"
51 fi
52
53 need_cmd() {
54 command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1"
55 }
56
57 download() {
58 url="$1"
59 out="$2"
60 if command -v curl >/dev/null 2>&1; then
61 curl -fsSL "$url" -o "$out"
62 elif command -v wget >/dev/null 2>&1; then
63 wget -q "$url" -O "$out"
64 else
65 fail "curl or wget is required"
66 fi
67 }
68
69 sha256_file() {
70 file="$1"
71 if command -v sha256sum >/dev/null 2>&1; then
72 sha256sum "$file" | awk '{print $1}'
73 elif command -v shasum >/dev/null 2>&1; then
74 shasum -a 256 "$file" | awk '{print $1}'
75 else
76 fail "sha256sum or shasum is required to verify downloads"
77 fi
78 }
79
80 verify_asset() {
81 asset="$1"
82 file="$2"
83 manifest="$3"
84 expected="$(
85 awk -v name="$asset" '
86 {
87 digest = tolower($1)
88 file = $2
89 sub(/^\*/, "", file)
90 if (file == name && digest ~ /^[0-9a-f]{64}$/) {
91 print digest
92 exit
93 }
94 }
95 ' "$manifest"
96 )"
97 [ -n "$expected" ] || fail "checksum not found for $asset"
98 actual="$(sha256_file "$file" | tr '[:upper:]' '[:lower:]')"
99 [ "$actual" = "$expected" ] || fail "checksum mismatch for $asset"
100 }
101
102 glibc_version() {
103 if command -v getconf >/dev/null 2>&1; then
104 getconf GNU_LIBC_VERSION 2>/dev/null | awk '{ print $NF; exit }'
105 return
106 fi
107 if command -v ldd >/dev/null 2>&1; then
108 ldd --version 2>/dev/null | awk 'NR == 1 {
109 for (i = 1; i <= NF; i++) {
110 if ($i ~ /^[0-9]+\.[0-9]+/) {
111 print $i
112 exit
113 }
114 }
115 }'
116 fi
117 }
118
119 version_at_least() {
120 have="$1"
121 need="$2"
122 awk -v have="$have" -v need="$need" '
123 BEGIN {
124 split(have, h, ".")
125 split(need, n, ".")
126 for (i = 1; i <= 3; i++) {
127 hv = h[i] + 0
128 nv = n[i] + 0
129 if (hv > nv) exit 0
130 if (hv < nv) exit 1
131 }
132 exit 0
133 }
134 '
135 }
136
137 check_glibc() {
138 case "$target" in
139 linux-arm64) ;;
140 *) return ;;
141 esac
142
143 # Linux arm64 assets became static musl builds in v0.9.6. `latest` and
144 # explicit v0.9.6+ installs therefore have no glibc floor. Keep the
145 # preflight only for explicitly requested older releases, whose arm64
146 # assets were linked against GNU libc on Ubuntu 24.04.
147 if [ "$version" = "latest" ]; then
148 return
149 fi
150 numeric_version="${version#v}"
151 if awk -v have="$numeric_version" '
152 BEGIN {
153 if (have !~ /^[0-9]+\.[0-9]+\.[0-9]+$/) exit 1
154 split(have, h, ".")
155 if (h[1] > 0) exit 0
156 if (h[1] < 0) exit 1
157 if (h[2] > 9) exit 0
158 if (h[2] < 9) exit 1
159 exit !(h[3] >= 6)
160 }
161 '; then
162 return
163 fi
164
165 [ "${CODEWHALE_SKIP_GLIBC_CHECK:-}" = "1" ] && return
166 [ "${DEEPSEEK_TUI_SKIP_GLIBC_CHECK:-}" = "1" ] && return
167 [ "${DEEPSEEK_SKIP_GLIBC_CHECK:-}" = "1" ] && return
168
169 required="2.39"
170 host="$(glibc_version || true)"
171 if [ -z "$host" ] || ! version_at_least "$host" "$required"; then
172 cat >&2 <<EOF
173 codewhale install: Codewhale $version $target assets require glibc $required or newer.
174 This system reports glibc ${host:-unavailable}.
175
176 Linux arm64 assets before v0.9.6 were GNU libc builds from Ubuntu 24.04.
177 Current v0.9.6+ assets are static musl builds. Build this older release from
178 source with Cargo or set
179 CODEWHALE_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk.
180 EOF
181 exit 1
182 fi
183 }
184
185 detect_platform() {
186 os="$(uname -s)"
187 arch="$(uname -m)"
188
189 if [ -n "${TERMUX_VERSION:-}" ] || [ "$(uname -o 2>/dev/null || true)" = "Android" ]; then
190 fail "Android/Termux needs the Android arm64 preview archive, not a Linux binary. See https://github.com/Hmbown/CodeWhale/blob/main/docs/INSTALL.md"
191 fi
192
193 case "$os" in
194 Darwin) platform="macos" ;;
195 Linux) platform="linux" ;;
196 *) fail "unsupported OS: $os. Use the matching asset at https://github.com/Hmbown/CodeWhale/releases/latest; npm and Cargo are secondary options." ;;
197 esac
198
199 case "$arch" in
200 x86_64|amd64) cpu="x64" ;;
201 arm64|aarch64) cpu="arm64" ;;
202 riscv64) fail "Linux riscv64 prebuilt assets are temporarily unavailable because the locked rquickjs-sys dependency does not ship riscv64gc bindings." ;;
203 *) fail "unsupported CPU architecture: $arch. Use Cargo or build from source." ;;
204 esac
205
206 printf '%s-%s' "$platform" "$cpu"
207 }
208
209 if [ -z "$release_base" ]; then
210 if [ "$version" = "latest" ]; then
211 release_base="https://github.com/$repo/releases/latest/download"
212 else
213 release_base="https://github.com/$repo/releases/download/$version"
214 fi
215 fi
216
217 target="$(detect_platform)"
218 check_glibc
219 cli_asset="codewhale-$target"
220 shim_asset="codew-$target"
221 manifest_asset="codewhale-artifacts-sha256.txt"
222
223 tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t codewhale-install)"
224 trap 'rm -rf "$tmpdir"' EXIT INT TERM
225
226 say "Installing Codewhale for $target"
227 say "Release assets: $release_base"
228 say "Install dir: $install_dir"
229
230 download "$release_base/$manifest_asset" "$tmpdir/$manifest_asset"
231 download "$release_base/$cli_asset" "$tmpdir/codewhale"
232 download "$release_base/$shim_asset" "$tmpdir/codew"
233
234 verify_asset "$cli_asset" "$tmpdir/codewhale" "$tmpdir/$manifest_asset"
235 verify_asset "$shim_asset" "$tmpdir/codew" "$tmpdir/$manifest_asset"
236 say "Checksums verified"
237
238 chmod 755 "$tmpdir/codewhale" "$tmpdir/codew"
239 if command -v xattr >/dev/null 2>&1; then
240 xattr -d com.apple.quarantine "$tmpdir/codewhale" "$tmpdir/codew" 2>/dev/null || true
241 fi
242
243 # Resolve the real directory before applying managed-prefix checks. Never use
244 # sudo or allow an install directory symlink to obscure which files will change.
245 case "$install_dir" in
246 /*) ;;
247 *) fail "CODEWHALE_INSTALL_DIR must be an absolute path" ;;
248 esac
249 [ ! -L "$install_dir" ] || fail "install directory is a symlink: $install_dir; choose a fresh user directory"
250 mkdir -p "$install_dir" || fail "cannot create $install_dir; choose a writable user directory (no sudo is used)"
251 install_dir="$(cd -P "$install_dir" && pwd)"
252 case "$install_dir/" in
253 /bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/nix/store/*|/gnu/store/*|*/node_modules/*|*/Cellar/*|*/.linuxbrew/*|*/linuxbrew/*|*/.cargo/bin/*)
254 fail "refusing managed/system directory $install_dir; use a fresh user directory"
255 ;;
256 esac
257 [ -w "$install_dir" ] || fail "$install_dir is not writable; choose a user directory (no sudo is used)"
258
259 check_destination() {
260 destination="$1"
261 source="$2"
262 destination_exists=0
263 if [ -e "$destination" ] || [ -L "$destination" ]; then
264 if [ ! -L "$destination" ] && [ -f "$destination" ] && [ -x "$destination" ] && cmp -s "$source" "$destination"; then
265 destination_exists=1
266 return
267 fi
268 cat >&2 <<EOF
269 codewhale install: refusing to replace existing $destination.
270 It may be a newer build, another installation, or a symlink. No existing file was changed.
271 For an existing direct Codewhale install, run its full path with 'update'.
272 To migrate safely from a package manager or mixed installation, create a fresh directory:
273 mkdir -p "\$HOME/.local"
274 codewhale_install_dir="\$(mktemp -d "\$HOME/.local/codewhale-release.XXXXXX")"
275 curl -fsSL https://codewhale.net/install.sh | CODEWHALE_INSTALL_DIR="\$codewhale_install_dir" sh
276 "\$codewhale_install_dir/codewhale" --version
277 export PATH="\$codewhale_install_dir:\$PATH"
278 hash -r
279 command -v codewhale codew
280 EOF
281 exit 1
282 fi
283 }
284
285 # Check every command before publishing any of them. Existing identical release
286 # files are an idempotent install; anything different uses the canonical updater.
287 check_destination "$install_dir/codewhale" "$tmpdir/codewhale"
288 check_destination "$install_dir/codew" "$tmpdir/codew"
289 legacy_tui="$install_dir/codewhale-tui"
290 if [ -e "$legacy_tui" ] || [ -L "$legacy_tui" ]; then
291 check_destination "$legacy_tui" "$tmpdir/codewhale"
292 fi
293
294 stage=""
295 stage_dir=""
296 trap 'rm -rf "$tmpdir"; if [ -n "$stage" ]; then rm -f "$stage"; fi; if [ -n "$stage_dir" ]; then rmdir "$stage_dir"; fi' EXIT INT TERM
297 install_binary() {
298 source="$1"
299 destination="$2"
300 # Recheck immediately before publication. Never replace a file another
301 # process created since preflight: linking the staged inode is no-clobber.
302 check_destination "$destination" "$source"
303 if [ "$destination_exists" -eq 1 ]; then
304 say "Already installed: $destination"
305 return
306 fi
307 stage_dir="$(mktemp -d "$install_dir/.codewhale-install.XXXXXX")"
308 stage="$stage_dir/$(basename "$destination")"
309 cp "$source" "$stage"
310 chmod 755 "$stage"
311 # Pass the intended parent as the directory operand. Passing destination
312 # itself would make ln treat a raced-in directory/symlink as a container.
313 ln "$stage" "$install_dir/" || fail "destination appeared during install: $destination; it was not replaced"
314 [ ! -L "$destination" ] && [ -f "$destination" ] && cmp -s "$stage" "$destination" || fail "installed path changed during publication: $destination"
315 rm -f "$stage"
316 rmdir "$stage_dir"
317 stage=""
318 stage_dir=""
319 }
320
321 install_binary "$tmpdir/codewhale" "$install_dir/codewhale"
322 install_binary "$tmpdir/codew" "$install_dir/codew"
323
324 say "Installed checksummed release commands:"
325 say " $install_dir/codewhale"
326 say " $install_dir/codew"
327
328 say ""
329 say "Use this installation: \"$install_dir/codewhale\""
330 say "Future updates: \"$install_dir/codewhale\" update"
331 for command_name in codewhale codew; do
332 resolved="$(command -v "$command_name" 2>/dev/null || true)"
333 if [ "$resolved" != "$install_dir/$command_name" ]; then
334 say "PATH selects ${resolved:-no $command_name command}; this install is $install_dir/$command_name"
335 fi
336 done
337 say "To use this directory in the current shell, then verify the commands:"
338 say " export PATH=\"$install_dir:\$PATH\""
339 say " hash -r"
340 say " command -v codewhale codew"
341 say "Keep the directory first in your shell profile after verifying it."
342 if ! command -v node >/dev/null 2>&1; then
343 say "Computer Use is included and needs Node.js 20 or newer on PATH."
344 say "Install Node.js from https://nodejs.org/, then restart Codewhale to enable Computer Use."
345 fi
346
346 lines BASH