fix(installer): handle alpine apk runtime floors

This commit is contained in:
Vincent Koc
2026-05-25 18:58:38 +02:00
parent bd65b4232a
commit b83dfcb953
4 changed files with 261 additions and 15 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Installer: make Alpine apk installs cover Git, verify the Node runtime floor, try `nodejs-current`, and report Alpine version guidance when repositories only provide older Node packages.
- Agents/media: send direct fallback for generated media still missing after an active requester wake fails. (#85489) Thanks @fuller-stack-dev.
- Agents: derive overflow compaction budgets from provider-reported and synthetic over-budget token counts so confirmed context overflows compact before retrying. (#70473) Thanks @fuller-stack-dev.
- Agents/Codex: recover Codex context-window prompt errors through overflow compaction and surface reset guidance when recovery is exhausted. (#85542) Thanks @fuller-stack-dev.

View File

@@ -72,9 +72,10 @@ Recommended for most interactive installs on macOS/Linux/WSL.
</Step>
<Step title="Ensure Node.js 24 by default">
Checks Node version and installs Node 24 if needed (Homebrew on macOS, NodeSource setup scripts on Linux apt/dnf/yum). OpenClaw still supports Node 22 LTS, currently `22.19+`, for compatibility.
On Alpine/musl Linux, the installer uses apk packages instead of NodeSource; the configured Alpine repositories must provide Node `22.19+` (Alpine 3.21 or newer at the time of writing).
</Step>
<Step title="Ensure Git">
Installs Git if missing.
Installs Git if missing using the detected package manager, including apk on Alpine.
</Step>
<Step title="Install OpenClaw">
- `npm` method (default): global npm install
@@ -187,10 +188,10 @@ by default, plus git-checkout installs under the same prefix flow.
<Steps>
<Step title="Install local Node runtime">
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.
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.
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. The Alpine repositories must provide Node `22.19+`; use Alpine 3.21 or newer if older repositories only provide Node 20 or 21.
</Step>
<Step title="Ensure Git">
If Git is missing, attempts install via apt/dnf/yum on Linux or Homebrew on macOS.
If Git is missing, attempts install via apt/dnf/yum/apk on Linux or Homebrew on macOS.
</Step>
<Step title="Install OpenClaw under prefix">
- `npm` method (default): installs under the prefix with npm, then writes wrapper to `<prefix>/bin/openclaw`

View File

@@ -624,6 +624,21 @@ is_arch_linux() {
return 1
}
is_alpine_linux() {
if [[ -f /etc/alpine-release ]]; then
return 0
fi
if [[ -f /etc/os-release ]]; then
local os_id os_id_like
os_id="$(grep -E '^ID=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true)"
os_id_like="$(grep -E '^ID_LIKE=' /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '"' || true)"
if [[ "$os_id" == "alpine" || "$os_id_like" == *alpine* ]]; then
return 0
fi
fi
return 1
}
apt_get() {
if is_root; then
env DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}" NEEDRESTART_MODE="${NEEDRESTART_MODE:-a}" apt-get "$@"
@@ -679,7 +694,7 @@ install_build_tools_linux() {
return 0
fi
if command -v apk &> /dev/null; then
if command -v apk &> /dev/null && is_alpine_linux; then
if is_root; then
run_quiet_step "Installing build tools" apk add --no-cache build-base python3 cmake
else
@@ -1715,6 +1730,44 @@ finish_linux_node_install() {
print_active_node_paths || true
}
install_node_with_apk() {
ui_info "Installing Node.js via apk (Alpine Linux detected)"
if is_root; then
run_quiet_step "Installing Node.js" apk add --no-cache nodejs npm
else
run_quiet_step "Installing Node.js" sudo apk add --no-cache nodejs npm
fi
activate_supported_node_on_path || true
if node_is_at_least_required; then
finish_linux_node_install
return 0
fi
local apk_node_version
apk_node_version="$(node -v 2>/dev/null || echo "missing")"
ui_warn "Alpine nodejs package installed ${apk_node_version}, below required v${NODE_MIN_VERSION}+"
ui_info "Trying Alpine nodejs-current package"
if is_root; then
run_quiet_step "Installing nodejs-current" apk add --no-cache nodejs-current npm
else
run_quiet_step "Installing nodejs-current" sudo apk add --no-cache nodejs-current npm
fi
activate_supported_node_on_path || true
if node_is_at_least_required; then
finish_linux_node_install
return 0
fi
local active_path active_version
active_path="$(command -v node 2>/dev/null || echo "not found")"
active_version="$(node -v 2>/dev/null || echo "missing")"
ui_error "Alpine apk repositories did not provide Node.js v${NODE_MIN_VERSION}+; found ${active_version} (${active_path})"
echo "Use Alpine 3.21+ or install Node.js ${NODE_DEFAULT_MAJOR} manually, then rerun the installer."
exit 1
}
# Install Node.js
install_node() {
if [[ "$OS" == "macos" ]]; then
@@ -1751,14 +1804,8 @@ install_node() {
return 0
fi
if command -v apk &> /dev/null; then
ui_info "Installing Node.js via apk (Alpine Linux detected)"
if is_root; then
run_quiet_step "Installing Node.js" apk add --no-cache nodejs npm
else
run_quiet_step "Installing Node.js" sudo apk add --no-cache nodejs npm
fi
finish_linux_node_install
if command -v apk &> /dev/null && is_alpine_linux; then
install_node_with_apk
return 0
fi
@@ -1857,7 +1904,13 @@ install_git() {
run_quiet_step "Installing Git" brew install git
elif [[ "$OS" == "linux" ]]; then
require_sudo
if command -v apt-get &> /dev/null; then
if command -v apk &> /dev/null && is_alpine_linux; then
if is_root; then
run_quiet_step "Installing Git" apk add --no-cache git
else
run_quiet_step "Installing Git" sudo apk add --no-cache git
fi
elif command -v apt-get &> /dev/null; then
run_quiet_step "Updating package index" apt_get_update
run_quiet_step "Installing Git" apt_get_install git
elif command -v pacman &> /dev/null || is_arch_linux; then

View File

@@ -109,14 +109,17 @@ describe("install.sh", () => {
it("installs Node.js with apk on Alpine before falling back to NodeSource", () => {
expect(script).toContain("finish_linux_node_install()");
expect(script).toContain("is_alpine_linux()");
expect(script).toContain("install_node_with_apk()");
expect(script).toContain('ui_info "Installing Node.js via apk (Alpine Linux detected)"');
expect(script).toContain('run_quiet_step "Installing Node.js" apk add --no-cache nodejs npm');
expect(script).toContain(
'run_quiet_step "Installing Node.js" sudo apk add --no-cache nodejs npm',
);
expect(script).toContain('run_quiet_step "Installing nodejs-current" apk add --no-cache nodejs-current npm');
expect(script).toContain("if ! node_is_at_least_required; then");
const apkIndex = script.indexOf("if command -v apk &> /dev/null; then");
const apkIndex = script.indexOf("if command -v apk &> /dev/null && is_alpine_linux; then");
const nodeSourceIndex = script.indexOf('ui_info "Installing Node.js via NodeSource"');
expect(apkIndex).toBeGreaterThan(-1);
expect(nodeSourceIndex).toBeGreaterThan(apkIndex);
@@ -130,10 +133,12 @@ describe("install.sh", () => {
require_sudo() { :; }
install_build_tools_linux() { return 0; }
is_root() { return 0; }
is_alpine_linux() { return 0; }
ui_info() { printf 'info:%s\\n' "$*"; }
ui_success() { printf 'success:%s\\n' "$*"; }
run_quiet_step() { printf 'step:%s|%s\\n' "$1" "\${*:2}"; }
apk() { :; }
node_is_at_least_required() { return 0; }
finish_linux_node_install() { printf 'finish-linux-node\\n'; }
install_node
`);
@@ -145,6 +150,182 @@ describe("install.sh", () => {
expect(result.stdout).not.toContain("Installing Node.js via NodeSource");
});
it("tries nodejs-current when Alpine nodejs is below the runtime floor", () => {
const result = runInstallShell(`
set -euo pipefail
source "${SCRIPT_PATH}"
OS=linux
NODE_FAKE_VERSION=v20.15.1
require_sudo() { :; }
install_build_tools_linux() { return 0; }
is_root() { return 0; }
is_alpine_linux() { return 0; }
ui_info() { printf 'info:%s\\n' "$*"; }
ui_success() { printf 'success:%s\\n' "$*"; }
ui_warn() { printf 'warn:%s\\n' "$*"; }
run_quiet_step() {
printf 'step:%s|%s\\n' "$1" "\${*:2}"
"\${@:2}"
}
apk() {
printf 'apk:%s\\n' "$*"
if [[ "$*" == *"nodejs-current"* ]]; then
NODE_FAKE_VERSION=v22.22.2
fi
}
node() {
if [[ "\${1:-}" == "-v" ]]; then
printf '%s\\n' "$NODE_FAKE_VERSION"
fi
}
activate_supported_node_on_path() { :; }
finish_linux_node_install() { printf 'finish-linux-node\\n'; }
install_node
`);
expect(result.status).toBe(0);
expect(result.stdout).toContain("step:Installing Node.js|apk add --no-cache nodejs npm");
expect(result.stdout).toContain("warn:Alpine nodejs package installed v20.15.1");
expect(result.stdout).toContain("step:Installing nodejs-current|apk add --no-cache nodejs-current npm");
expect(result.stdout).toContain("finish-linux-node");
});
it("fails with Alpine version guidance when apk cannot provide the runtime floor", () => {
const result = runInstallShell(`
set -euo pipefail
source "${SCRIPT_PATH}"
OS=linux
NODE_FAKE_VERSION=v20.15.1
require_sudo() { :; }
install_build_tools_linux() { return 0; }
is_root() { return 0; }
is_alpine_linux() { return 0; }
ui_info() { printf 'info:%s\\n' "$*"; }
ui_success() { printf 'success:%s\\n' "$*"; }
ui_warn() { printf 'warn:%s\\n' "$*"; }
ui_error() { printf 'error:%s\\n' "$*"; }
run_quiet_step() {
printf 'step:%s|%s\\n' "$1" "\${*:2}"
"\${@:2}"
}
apk() {
printf 'apk:%s\\n' "$*"
if [[ "$*" == *"nodejs-current"* ]]; then
NODE_FAKE_VERSION=v21.7.3
fi
}
node() {
if [[ "\${1:-}" == "-v" ]]; then
printf '%s\\n' "$NODE_FAKE_VERSION"
fi
}
activate_supported_node_on_path() { :; }
install_node
`);
expect(result.status).toBe(1);
expect(result.stdout).toContain("warn:Alpine nodejs package installed v20.15.1");
expect(result.stdout).toContain("step:Installing nodejs-current|apk add --no-cache nodejs-current npm");
expect(result.stdout).toContain("error:Alpine apk repositories did not provide Node.js v22.19+");
expect(result.stdout).toContain("Use Alpine 3.21+ or install Node.js 24 manually");
});
it("installs Git with apk on Alpine", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-git-apk-"));
const bin = join(tmp, "bin");
const apkLog = join(tmp, "apk-args.txt");
mkdirSync(bin, { recursive: true });
const fakeApk = join(bin, "apk");
writeFileSync(
fakeApk,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
`printf '%s\\n' "$*" >> ${JSON.stringify(apkLog)}`,
"",
].join("\n"),
);
chmodSync(fakeApk, 0o755);
try {
const result = runInstallShell(`
set -euo pipefail
source "${SCRIPT_PATH}"
PATH=${JSON.stringify(`${bin}:/bin`)}
OS=linux
require_sudo() { :; }
is_root() { return 0; }
is_alpine_linux() { return 0; }
ui_success() { printf 'success:%s\\n' "$*"; }
ui_error() { printf 'error:%s\\n' "$*"; }
run_quiet_step() {
printf 'step:%s|%s\\n' "$1" "\${*:2}"
"\${@:2}"
}
install_git
`);
expect(result.status).toBe(0);
expect(result.stdout).toContain("step:Installing Git|apk add --no-cache git");
expect(result.stdout).toContain("success:Git installed");
expect(readFileSync(apkLog, "utf8").trim()).toBe("add --no-cache git");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("does not select apk Git on non-Alpine hosts", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-git-native-"));
const bin = join(tmp, "bin");
const apkLog = join(tmp, "apk-args.txt");
mkdirSync(bin, { recursive: true });
const fakeApk = join(bin, "apk");
const fakeApt = join(bin, "apt-get");
writeFileSync(apkLog, "");
writeFileSync(
fakeApk,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
`printf '%s\\n' "$*" >> ${JSON.stringify(apkLog)}`,
"",
].join("\n"),
);
writeFileSync(fakeApt, "#!/usr/bin/env bash\nexit 0\n");
chmodSync(fakeApk, 0o755);
chmodSync(fakeApt, 0o755);
try {
const result = runInstallShell(`
set -euo pipefail
source "${SCRIPT_PATH}"
PATH=${JSON.stringify(`${bin}:/bin`)}
OS=linux
require_sudo() { :; }
is_root() { return 0; }
is_alpine_linux() { return 1; }
apt_get_update() { printf 'apt-update\\n'; }
apt_get_install() { printf 'apt-install:%s\\n' "$*"; }
ui_success() { printf 'success:%s\\n' "$*"; }
ui_error() { printf 'error:%s\\n' "$*"; }
run_quiet_step() {
printf 'step:%s|%s\\n' "$1" "\${*:2}"
"\${@:2}"
}
install_git
`);
expect(result.status).toBe(0);
expect(result.stdout).toContain("step:Updating package index|apt_get_update");
expect(result.stdout).toContain("apt-update");
expect(result.stdout).toContain("step:Installing Git|apt_get_install git");
expect(result.stdout).toContain("apt-install:git");
expect(readFileSync(apkLog, "utf8")).toBe("");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("clears npm freshness filters for package installs", () => {
expect(script).toContain("env -u NPM_CONFIG_BEFORE -u npm_config_before");
expect(script).toContain('freshness_flag="--min-release-age=0"');
@@ -156,10 +337,12 @@ describe("install.sh", () => {
it("does not emit --before when raw user npmrc config contains min-release-age", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-npmrc-"));
const bin = join(tmp, "bin");
const home = join(tmp, "home");
const npmrc = join(tmp, "user.npmrc");
const calls = join(tmp, "npm-calls.txt");
const installArgs = join(tmp, "npm-install-args.txt");
mkdirSync(bin, { recursive: true });
mkdirSync(home, { recursive: true });
writeFileSync(npmrc, "min-release-age=7\n");
const fakeNpm = join(bin, "npm");
writeFileSync(
@@ -194,10 +377,11 @@ describe("install.sh", () => {
'printf "cmd=%s\\n" "$LAST_NPM_INSTALL_CMD"',
].join("\n"),
{
HOME: home,
NPM_CONFIG_USERCONFIG: npmrc,
NPM_FAKE_CALLS: calls,
NPM_FAKE_INSTALL_ARGS: installArgs,
PATH: `${bin}:${process.env.PATH}`,
PATH: `${bin}:/usr/local/bin:/usr/bin:/bin`,
},
);
@@ -638,6 +822,13 @@ describe("install.sh", () => {
[
`cd ${JSON.stringify(process.cwd())}`,
`source ${JSON.stringify(SCRIPT_PATH)}`,
"type() {",
' if [[ "$*" == "-P -a node" ]]; then',
` printf '%s\\n' ${JSON.stringify(staleNode)} ${JSON.stringify(supportedNode)}`,
" return 0",
" fi",
' builtin type "$@"',
"}",
"set +e",
"OS=linux",
"promote_supported_node_binary",