Skip to content

Commit f68ed72

Browse files
committed
fix(installer): support alpine cli installs
1 parent 2a73725 commit f68ed72

4 files changed

Lines changed: 584 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
88

99
### Fixes
1010

11+
- Installer: let the local-prefix CLI installer use Alpine's `apk` Node.js, npm, and Git packages on musl Linux instead of downloading glibc Node tarballs that fail `node:sqlite`.
1112
- Scripts: use `git grep` to prefilter tracked conflict-marker scans so changed checks avoid reading every repository file on clean runs.
1213
- Installer: install Node.js through `apk` on Alpine Linux instead of falling through to the NodeSource package-manager path.
1314
- Agents/perf: cache manifest-backed CLI provider descriptors and fallback provider resolution so model fallback retries avoid repeated bundled provider runtime scans while still invalidating across plugin reloads.

docs/install/installer.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ by default, plus git-checkout installs under the same prefix flow.
189189
<Steps>
190190
<Step title="Install local Node runtime">
191191
Downloads a pinned supported Node LTS tarball (the version is embedded in the script and updated independently) to `<prefix>/tools/node-v<version>` and verifies SHA-256.
192+
On Alpine/musl Linux, where Node does not publish compatible tarballs for the pinned runtime, installs `nodejs` and `npm` with `apk` and links that runtime into the prefix wrapper path.
192193
</Step>
193194
<Step title="Ensure Git">
194195
If Git is missing, attempts install via apt/dnf/yum on Linux or Homebrew on macOS.

scripts/install-cli.sh

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ OPENCLAW_EFFECTIVE_HOME="$(resolve_openclaw_effective_home)"
5353
PREFIX="${OPENCLAW_PREFIX:-${HOME}/.openclaw}"
5454
OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
5555
NODE_VERSION="${OPENCLAW_NODE_VERSION:-22.22.0}"
56+
NODE_VERSION_REQUESTED=0
57+
if [[ -n "${OPENCLAW_NODE_VERSION:-}" ]]; then
58+
NODE_VERSION_REQUESTED=1
59+
fi
60+
MIN_NODE_VERSION="22.19.0"
61+
APK_NODE_BIN_DIR="/usr/bin"
5662
SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-1}"
5763
NPM_LOGLEVEL="${OPENCLAW_NPM_LOGLEVEL:-error}"
5864
INSTALL_METHOD="${OPENCLAW_INSTALL_METHOD:-npm}"
@@ -215,6 +221,14 @@ ensure_git() {
215221
else
216222
fail "Git missing and sudo unavailable. Install git and retry."
217223
fi
224+
elif command -v apk >/dev/null 2>&1; then
225+
if is_root; then
226+
apk add --no-cache git
227+
elif has_sudo; then
228+
sudo apk add --no-cache git
229+
else
230+
fail "Git missing and sudo unavailable. Install git and retry."
231+
fi
218232
else
219233
fail "Git missing and package manager not found. Install git and retry."
220234
fi
@@ -252,6 +266,7 @@ parse_args() {
252266
;;
253267
--node-version)
254268
NODE_VERSION="$2"
269+
NODE_VERSION_REQUESTED=1
255270
shift 2
256271
;;
257272
--install-method|--method)
@@ -329,6 +344,177 @@ npm_bin() {
329344
echo "$(node_dir)/bin/npm"
330345
}
331346

347+
command_path_without_node_prefix() {
348+
local name="$1"
349+
local path_entry
350+
local prefix_bin
351+
local filtered_path=""
352+
local separator=""
353+
local -a path_entries=()
354+
355+
prefix_bin="$(node_dir)/bin"
356+
IFS=: read -r -a path_entries <<<"$PATH"
357+
for path_entry in "${path_entries[@]}"; do
358+
if [[ "$path_entry" == "$prefix_bin" ]]; then
359+
continue
360+
fi
361+
filtered_path="${filtered_path}${separator}${path_entry}"
362+
separator=":"
363+
done
364+
365+
PATH="$filtered_path" command -v "$name" 2>/dev/null
366+
}
367+
368+
is_musl_linux() {
369+
if [[ "$(os_detect)" != "linux" ]]; then
370+
return 1
371+
fi
372+
if [[ -f /etc/alpine-release ]]; then
373+
return 0
374+
fi
375+
ldd --version 2>&1 | grep -qi musl
376+
}
377+
378+
link_node_runtime_paths() {
379+
local node_path="$1"
380+
local npm_path="$2"
381+
local dir
382+
local runtime_bin
383+
local resolved
384+
dir="$(node_dir)"
385+
runtime_bin="${node_path%/*}"
386+
387+
mkdir -p "${dir}/bin" "${PREFIX}/tools"
388+
ln -sfn "$node_path" "${dir}/bin/node"
389+
ln -sfn "$npm_path" "${dir}/bin/npm"
390+
for name in npx corepack; do
391+
if [[ -x "${runtime_bin}/${name}" ]]; then
392+
ln -sfn "${runtime_bin}/${name}" "${dir}/bin/${name}"
393+
continue
394+
fi
395+
resolved="$(command_path_without_node_prefix "$name" || true)"
396+
if [[ -n "$resolved" && "$resolved" != "${dir}/bin/${name}" ]]; then
397+
ln -sfn "$resolved" "${dir}/bin/${name}"
398+
fi
399+
done
400+
ln -sfn "$dir" "${PREFIX}/tools/node"
401+
}
402+
403+
linked_node_is_usable() {
404+
local current_version
405+
local required_version
406+
407+
if [[ ! -x "$(node_bin)" || ! -x "$(npm_bin)" ]]; then
408+
return 1
409+
fi
410+
411+
current_version="$("$(node_bin)" -v 2>/dev/null || echo "")"
412+
required_version="$(required_node_version)"
413+
if ! semver_at_least "$current_version" "$required_version"; then
414+
return 1
415+
fi
416+
417+
"$(node_bin)" -e "require('node:sqlite')" >/dev/null 2>&1
418+
}
419+
420+
semver_at_least() {
421+
local version="${1#v}"
422+
local required="${2#v}"
423+
local version_major version_minor version_patch
424+
local required_major required_minor required_patch
425+
426+
IFS=. read -r version_major version_minor version_patch <<<"$version"
427+
IFS=. read -r required_major required_minor required_patch <<<"$required"
428+
version_minor="${version_minor:-0}"
429+
version_patch="${version_patch:-0}"
430+
required_minor="${required_minor:-0}"
431+
required_patch="${required_patch:-0}"
432+
433+
for part in "$version_major" "$version_minor" "$version_patch" "$required_major" "$required_minor" "$required_patch"; do
434+
if [[ ! "$part" =~ ^[0-9]+$ ]]; then
435+
return 1
436+
fi
437+
done
438+
439+
if ((version_major != required_major)); then
440+
((version_major > required_major))
441+
return
442+
fi
443+
if ((version_minor != required_minor)); then
444+
((version_minor > required_minor))
445+
return
446+
fi
447+
((version_patch >= required_patch))
448+
}
449+
450+
required_node_version() {
451+
if [[ "$NODE_VERSION_REQUESTED" == "1" ]] && semver_at_least "$NODE_VERSION" "$MIN_NODE_VERSION"; then
452+
printf '%s\n' "$NODE_VERSION"
453+
return
454+
fi
455+
printf '%s\n' "$MIN_NODE_VERSION"
456+
}
457+
458+
try_link_usable_node_runtime_from_path() {
459+
local path_entry
460+
local prefix_bin
461+
local -a path_entries=()
462+
463+
prefix_bin="$(node_dir)/bin"
464+
IFS=: read -r -a path_entries <<<"$PATH"
465+
for path_entry in "${path_entries[@]}"; do
466+
if [[ -z "$path_entry" ]]; then
467+
path_entry="."
468+
fi
469+
if [[ "$path_entry" == "$prefix_bin" ]]; then
470+
continue
471+
fi
472+
if [[ -x "${path_entry}/node" && -x "${path_entry}/npm" ]]; then
473+
link_node_runtime_paths "${path_entry}/node" "${path_entry}/npm"
474+
if linked_node_is_usable; then
475+
return 0
476+
fi
477+
fi
478+
done
479+
return 1
480+
}
481+
482+
install_alpine_node() {
483+
local installed_version
484+
local required_version
485+
486+
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"start\",\"method\":\"apk\"}"
487+
if try_link_usable_node_runtime_from_path; then
488+
installed_version="$("$(node_bin)" -v 2>/dev/null || echo unknown)"
489+
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"ok\",\"method\":\"system\",\"version\":\"${installed_version}\"}"
490+
return
491+
fi
492+
493+
log "Installing Node via apk (Alpine Linux detected)..."
494+
if is_root; then
495+
apk add --no-cache nodejs npm
496+
elif has_sudo; then
497+
sudo apk add --no-cache nodejs npm
498+
else
499+
fail "Alpine Linux detected, but Node musl tarballs are unavailable and sudo is unavailable. Install nodejs and npm with apk, then retry."
500+
fi
501+
502+
if [[ -x "${APK_NODE_BIN_DIR}/node" && -x "${APK_NODE_BIN_DIR}/npm" ]]; then
503+
link_node_runtime_paths "${APK_NODE_BIN_DIR}/node" "${APK_NODE_BIN_DIR}/npm"
504+
elif ! try_link_usable_node_runtime_from_path; then
505+
fail "apk Node install failed. Install nodejs and npm manually, then retry."
506+
fi
507+
508+
if ! linked_node_is_usable; then
509+
installed_version="$("$(node_bin)" -v 2>/dev/null || echo unknown)"
510+
required_version="$(required_node_version)"
511+
fail "Alpine Node package must provide Node >= ${required_version} with node:sqlite; found ${installed_version}."
512+
fi
513+
514+
installed_version="$("$(node_bin)" -v 2>/dev/null || echo unknown)"
515+
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"ok\",\"method\":\"apk\",\"version\":\"${installed_version}\"}"
516+
}
517+
332518
set_pnpm_cmd() {
333519
PNPM_CMD=("$@")
334520
}
@@ -560,6 +746,11 @@ install_node() {
560746
arch="$(arch_detect)"
561747
dir="$(node_dir)"
562748

749+
if [[ "$os" == "linux" ]] && command -v apk >/dev/null 2>&1 && is_musl_linux; then
750+
install_alpine_node
751+
return
752+
fi
753+
563754
if [[ -x "$(node_bin)" ]]; then
564755
current_major="$("$(node_bin)" -v 2>/dev/null | tr -d 'v' | cut -d'.' -f1 || echo "")"
565756
if [[ -n "$current_major" && "$current_major" -ge 22 ]]; then

0 commit comments

Comments
 (0)