mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-29 18:05:16 +00:00
fix(installer): handle alpine apk runtime floors
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user