返回 CodeWhale
install.sh
根目录 / scripts / release / install.sh
1 #!/usr/bin/env bash
2 set -euo pipefail
3 # CodeWhale Unix installer
4 # Copies codewhale and codew to ~/.local/bin (or $PREFIX/bin)
5
6 PREFIX="${PREFIX:-$HOME/.local}"
7 BIN_DIR="${PREFIX}/bin"
8 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
10 version_code() {
11 local version="$1"
12 local major minor patch
13 IFS=. read -r major minor patch <<< "$version"
14 printf '%d%03d%03d\n' "${major:-0}" "${minor:-0}" "${patch:-0}"
15 }
16
17 detect_host_glibc() {
18 local out
19 if out="$(getconf GNU_LIBC_VERSION 2>/dev/null)"; then
20 printf '%s\n' "$out" | awk '{print $NF; exit}'
21 return 0
22 fi
23 if out="$(ldd --version 2>&1 | head -n 1)"; then
24 printf '%s\n' "$out" | grep -Eo '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -n 1
25 return 0
26 fi
27 return 1
28 }
29
30 required_glibc_for_binary() {
31 local bin="$1"
32 local versions
33 versions="$(grep -aoE 'GLIBC_[0-9]+\.[0-9]+(\.[0-9]+)?' "$bin" 2>/dev/null | sed 's/^GLIBC_//' || true)"
34 if [[ -z "$versions" ]]; then
35 return 1
36 fi
37 printf '%s\n' "$versions" | awk -F. '
38 {
39 patch = ($3 == "" ? 0 : $3)
40 code = ($1 * 1000000) + ($2 * 1000) + patch
41 if (code > best) {
42 best = code
43 value = $0
44 }
45 }
46 END {
47 if (value != "") print value
48 }
49 '
50 }
51
52 preflight_glibc() {
53 local bin="$1"
54 local role="${2:-CLI}"
55 if [[ "$(uname -s)" != "Linux" ]]; then
56 return 0
57 fi
58 if [[ "${role}" != "compiled host" && ( "${CODEWHALE_SKIP_GLIBC_CHECK:-}" == "1" || "${DEEPSEEK_TUI_SKIP_GLIBC_CHECK:-}" == "1" || "${DEEPSEEK_SKIP_GLIBC_CHECK:-}" == "1" ) ]]; then
59 return 0
60 fi
61
62 local required
63 if ! required="$(required_glibc_for_binary "$bin")" || [[ -z "$required" ]]; then
64 return 0
65 fi
66
67 local host
68 if ! host="$(detect_host_glibc)" || [[ -z "$host" ]]; then
69 echo "ERROR: $(basename "$bin") requires GLIBC_$required, but no GNU libc was detected." >&2
70 if [[ "$(basename "$bin")" == codewhale-extension-host ]]; then
71 echo "The optional compiled Bun image has a separate libc floor; use Node on this installation. The Codewhale CLI remains static musl." >&2
72 else
73 echo "Official Codewhale Linux release assets (x64 and arm64) are static musl builds" >&2
74 echo "with no glibc dependency, so this binary is not an official release asset." >&2
75 fi
76 echo "Check where it came from, or build from source on this host." >&2
77 echo "Build from source instead: cargo install codewhale-cli --locked" >&2
78 if [[ "$role" != "compiled host" ]]; then echo "Set CODEWHALE_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk." >&2; fi
79 return 1
80 fi
81
82 if [[ "$(version_code "$host")" -lt "$(version_code "$required")" ]]; then
83 echo "ERROR: $(basename "$bin") requires GLIBC_$required, but this system has glibc $host." >&2
84 if [[ "$(basename "$bin")" == codewhale-extension-host ]]; then
85 echo "The optional compiled Bun image has a separate libc floor; use Node on this installation. The Codewhale CLI remains static musl." >&2
86 else
87 echo "Official Codewhale Linux release assets (x64 and arm64) are static musl builds" >&2
88 echo "with no glibc dependency, so this binary is not an official release asset." >&2
89 fi
90 echo "Check where it came from, or build from source on this host." >&2
91 echo "Build from source instead: cargo install codewhale-cli --locked" >&2
92 if [[ "$role" != "compiled host" ]]; then echo "Set CODEWHALE_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk." >&2; fi
93 return 1
94 fi
95 }
96
97 # This script installs an already checksum-verified archive. Existing different
98 # binaries go through `codewhale update`, the sole version-aware updater.
99 case "$BIN_DIR" in
100 /*) ;;
101 *) echo "ERROR: PREFIX must be an absolute user path" >&2; exit 1 ;;
102 esac
103 [[ ! -L "$BIN_DIR" ]] || { echo "ERROR: $BIN_DIR is a symlink; choose a fresh PREFIX" >&2; exit 1; }
104 mkdir -p "$BIN_DIR"
105 BIN_DIR="$(cd -P "$BIN_DIR" && pwd)"
106 case "$BIN_DIR/" in
107 /bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/nix/store/*|/gnu/store/*|*/node_modules/*|*/Cellar/*|*/.linuxbrew/*|*/linuxbrew/*|*/.cargo/bin/*)
108 echo "ERROR: refusing managed/system directory $BIN_DIR; choose a fresh user PREFIX" >&2
109 exit 1
110 ;;
111 esac
112 [[ -w "$BIN_DIR" ]] || { echo "ERROR: $BIN_DIR is not writable; choose a user PREFIX (no sudo)" >&2; exit 1; }
113
114 check_destination() {
115 local src="$1" dst="$2" mode="${3:-0755}"
116 destination_exists=0
117 if [[ -e "$dst" || -L "$dst" ]]; then
118 if [[ ! -L "$dst" && -f "$dst" ]] && [[ "$mode" != 0755 || -x "$dst" ]] && cmp -s "$src" "$dst"; then
119 destination_exists=1
120 return 0
121 fi
122 echo "ERROR: refusing to replace existing $dst; no existing file was changed." >&2
123 echo "For an existing direct Codewhale install, run its full path with 'update'." >&2
124 echo "To migrate, install this verified archive into a fresh user prefix:" >&2
125 echo ' mkdir -p "$HOME/.local"' >&2
126 echo ' codewhale_prefix="$(mktemp -d "$HOME/.local/codewhale-release.XXXXXX")"' >&2
127 echo ' PREFIX="$codewhale_prefix" ./install.sh' >&2
128 echo ' "$codewhale_prefix/bin/codewhale" --version' >&2
129 echo "Put the selected prefix/bin first on PATH after verifying it; see docs/INSTALL.md." >&2
130 return 1
131 fi
132 }
133
134 # The image is an explicit installer choice; presence never changes Core's
135 # Node default. Archives carry notices/source beside any qualified image.
136 payloads=(codewhale codew)
137 if [[ "${CODEWHALE_INSTALL_COMPILED_HOST:-}" == 1 ]]; then
138 for file in codewhale-extension-host codewhale-extension-host.LICENSES.txt codewhale-extension-host.relink-source.tar.gz codewhale-extension-host.release.json; do
139 [[ -f "$SCRIPT_DIR/$file" && ! -L "$SCRIPT_DIR/$file" ]] || { echo "ERROR: compiled host requested but archive has no complete qualified payload ($file); use Node" >&2; exit 1; }
140 payloads+=("$file")
141 done
142 fi
143 # Validate every source and destination before the first write.
144 for bin in "${payloads[@]}"; do
145 src="$SCRIPT_DIR/$bin"
146 [[ -f "$src" ]] || { echo "ERROR: $src not found in archive" >&2; exit 1; }
147 mode=0644
148 case "$bin" in codewhale|codew|codewhale-extension-host) mode=0755; preflight_glibc "$src" "${bin/codewhale-extension-host/compiled host}" ;; esac
149 check_destination "$src" "$BIN_DIR/$bin" "$mode"
150 done
151 legacy_tui="$BIN_DIR/codewhale-tui"
152 if [[ -e "$legacy_tui" || -L "$legacy_tui" ]]; then
153 check_destination "$SCRIPT_DIR/codewhale" "$legacy_tui"
154 fi
155
156 stage=""
157 stage_dir=""
158 # Commands this run published, as "source<TAB>destination" lines. If a later
159 # publication fails, they are removed again (only while each is still the
160 # exact file this run wrote), so a failed install leaves no half-installed
161 # pair; files that were already installed are never touched.
162 published=""
163 rollback_published() {
164 local src dst
165 while IFS=$'\t' read -r src dst; do
166 [[ -n "$dst" ]] || continue
167 if [[ ! -L "$dst" && -f "$dst" ]] && cmp -s "$src" "$dst"; then
168 rm -f "$dst"
169 echo "Removed $dst: this install did not complete." >&2
170 fi
171 done <<< "$published"
172 }
173 on_exit() {
174 local status=$?
175 if [[ -n "$stage" ]]; then rm -f "$stage"; fi
176 if [[ -n "$stage_dir" ]]; then rmdir "$stage_dir"; fi
177 if [[ "$status" -ne 0 && -n "$published" ]]; then rollback_published; fi
178 }
179 trap on_exit EXIT
180 install_binary() {
181 local src="$1" dst="$2" mode="${3:-0755}"
182 check_destination "$src" "$dst" "$mode"
183 if [[ "$destination_exists" == 1 ]]; then
184 echo " $dst (already installed)"
185 return
186 fi
187 stage_dir="$(mktemp -d "$BIN_DIR/.codewhale-install.XXXXXX")"
188 stage="$stage_dir/$(basename "$dst")"
189 cp "$src" "$stage"
190 chmod "$mode" "$stage"
191 # Same-directory hard-link publication is atomic and never overwrites a
192 # destination created between preflight and this operation.
193 # The explicit parent operand avoids treating a raced-in destination
194 # directory (or directory symlink) as an alternate publication location.
195 ln "$stage" "$BIN_DIR/"
196 [[ ! -L "$dst" && -f "$dst" ]] && cmp -s "$stage" "$dst" || {
197 echo "ERROR: installed path changed during publication: $dst" >&2
198 return 1
199 }
200 published+="$src"$'\t'"$dst"$'\n'
201 rm -f "$stage"
202 rmdir "$stage_dir"
203 stage=""
204 stage_dir=""
205 echo " $dst"
206 }
207
208 echo "Installing codewhale to $BIN_DIR ..."
209 for bin in "${payloads[@]}"; do
210 mode=0644
211 case "$bin" in codewhale|codew|codewhale-extension-host) mode=0755 ;; esac
212 install_binary "$SCRIPT_DIR/$bin" "$BIN_DIR/$bin" "$mode"
213 done
214
215 echo ""
216 echo "Done. Commands installed to $BIN_DIR."
217 echo "Future updates: \"$BIN_DIR/codewhale\" update"
218 for bin in codewhale codew; do
219 resolved="$(command -v "$bin" || true)"
220 if [[ "$resolved" != "$BIN_DIR/$bin" ]]; then
221 echo "PATH selects ${resolved:-no $bin command}; this install is $BIN_DIR/$bin"
222 fi
223 done
224 echo "To select this installation in the current shell:"
225 echo " export PATH=\"$BIN_DIR:\$PATH\""
226 echo " hash -r"
227 echo " command -v codewhale codew"
228 echo "Keep the directory first in your shell profile after verifying it."
229 if ! command -v node >/dev/null 2>&1; then
230 echo "Computer Use is included and needs Node.js 20 or newer on PATH."
231 echo "Install Node.js from https://nodejs.org/, then restart Codewhale to enable Computer Use."
232 fi
233
233 lines BASH