mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(installer): support alpine cli installs
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- 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`.
|
||||
- Scripts: use `git grep` to prefilter tracked conflict-marker scans so changed checks avoid reading every repository file on clean runs.
|
||||
- Installer: install Node.js through `apk` on Alpine Linux instead of falling through to the NodeSource package-manager path.
|
||||
- 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.
|
||||
|
||||
@@ -189,6 +189,7 @@ 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.
|
||||
</Step>
|
||||
<Step title="Ensure Git">
|
||||
If Git is missing, attempts install via apt/dnf/yum on Linux or Homebrew on macOS.
|
||||
|
||||
@@ -53,6 +53,12 @@ OPENCLAW_EFFECTIVE_HOME="$(resolve_openclaw_effective_home)"
|
||||
PREFIX="${OPENCLAW_PREFIX:-${HOME}/.openclaw}"
|
||||
OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
|
||||
NODE_VERSION="${OPENCLAW_NODE_VERSION:-22.22.0}"
|
||||
NODE_VERSION_REQUESTED=0
|
||||
if [[ -n "${OPENCLAW_NODE_VERSION:-}" ]]; then
|
||||
NODE_VERSION_REQUESTED=1
|
||||
fi
|
||||
MIN_NODE_VERSION="22.19.0"
|
||||
APK_NODE_BIN_DIR="/usr/bin"
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-1}"
|
||||
NPM_LOGLEVEL="${OPENCLAW_NPM_LOGLEVEL:-error}"
|
||||
INSTALL_METHOD="${OPENCLAW_INSTALL_METHOD:-npm}"
|
||||
@@ -215,6 +221,14 @@ ensure_git() {
|
||||
else
|
||||
fail "Git missing and sudo unavailable. Install git and retry."
|
||||
fi
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
if is_root; then
|
||||
apk add --no-cache git
|
||||
elif has_sudo; then
|
||||
sudo apk add --no-cache git
|
||||
else
|
||||
fail "Git missing and sudo unavailable. Install git and retry."
|
||||
fi
|
||||
else
|
||||
fail "Git missing and package manager not found. Install git and retry."
|
||||
fi
|
||||
@@ -252,6 +266,7 @@ parse_args() {
|
||||
;;
|
||||
--node-version)
|
||||
NODE_VERSION="$2"
|
||||
NODE_VERSION_REQUESTED=1
|
||||
shift 2
|
||||
;;
|
||||
--install-method|--method)
|
||||
@@ -329,6 +344,177 @@ npm_bin() {
|
||||
echo "$(node_dir)/bin/npm"
|
||||
}
|
||||
|
||||
command_path_without_node_prefix() {
|
||||
local name="$1"
|
||||
local path_entry
|
||||
local prefix_bin
|
||||
local filtered_path=""
|
||||
local separator=""
|
||||
local -a path_entries=()
|
||||
|
||||
prefix_bin="$(node_dir)/bin"
|
||||
IFS=: read -r -a path_entries <<<"$PATH"
|
||||
for path_entry in "${path_entries[@]}"; do
|
||||
if [[ "$path_entry" == "$prefix_bin" ]]; then
|
||||
continue
|
||||
fi
|
||||
filtered_path="${filtered_path}${separator}${path_entry}"
|
||||
separator=":"
|
||||
done
|
||||
|
||||
PATH="$filtered_path" command -v "$name" 2>/dev/null
|
||||
}
|
||||
|
||||
is_musl_linux() {
|
||||
if [[ "$(os_detect)" != "linux" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ -f /etc/alpine-release ]]; then
|
||||
return 0
|
||||
fi
|
||||
ldd --version 2>&1 | grep -qi musl
|
||||
}
|
||||
|
||||
link_node_runtime_paths() {
|
||||
local node_path="$1"
|
||||
local npm_path="$2"
|
||||
local dir
|
||||
local runtime_bin
|
||||
local resolved
|
||||
dir="$(node_dir)"
|
||||
runtime_bin="${node_path%/*}"
|
||||
|
||||
mkdir -p "${dir}/bin" "${PREFIX}/tools"
|
||||
ln -sfn "$node_path" "${dir}/bin/node"
|
||||
ln -sfn "$npm_path" "${dir}/bin/npm"
|
||||
for name in npx corepack; do
|
||||
if [[ -x "${runtime_bin}/${name}" ]]; then
|
||||
ln -sfn "${runtime_bin}/${name}" "${dir}/bin/${name}"
|
||||
continue
|
||||
fi
|
||||
resolved="$(command_path_without_node_prefix "$name" || true)"
|
||||
if [[ -n "$resolved" && "$resolved" != "${dir}/bin/${name}" ]]; then
|
||||
ln -sfn "$resolved" "${dir}/bin/${name}"
|
||||
fi
|
||||
done
|
||||
ln -sfn "$dir" "${PREFIX}/tools/node"
|
||||
}
|
||||
|
||||
linked_node_is_usable() {
|
||||
local current_version
|
||||
local required_version
|
||||
|
||||
if [[ ! -x "$(node_bin)" || ! -x "$(npm_bin)" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
current_version="$("$(node_bin)" -v 2>/dev/null || echo "")"
|
||||
required_version="$(required_node_version)"
|
||||
if ! semver_at_least "$current_version" "$required_version"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
"$(node_bin)" -e "require('node:sqlite')" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
semver_at_least() {
|
||||
local version="${1#v}"
|
||||
local required="${2#v}"
|
||||
local version_major version_minor version_patch
|
||||
local required_major required_minor required_patch
|
||||
|
||||
IFS=. read -r version_major version_minor version_patch <<<"$version"
|
||||
IFS=. read -r required_major required_minor required_patch <<<"$required"
|
||||
version_minor="${version_minor:-0}"
|
||||
version_patch="${version_patch:-0}"
|
||||
required_minor="${required_minor:-0}"
|
||||
required_patch="${required_patch:-0}"
|
||||
|
||||
for part in "$version_major" "$version_minor" "$version_patch" "$required_major" "$required_minor" "$required_patch"; do
|
||||
if [[ ! "$part" =~ ^[0-9]+$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ((version_major != required_major)); then
|
||||
((version_major > required_major))
|
||||
return
|
||||
fi
|
||||
if ((version_minor != required_minor)); then
|
||||
((version_minor > required_minor))
|
||||
return
|
||||
fi
|
||||
((version_patch >= required_patch))
|
||||
}
|
||||
|
||||
required_node_version() {
|
||||
if [[ "$NODE_VERSION_REQUESTED" == "1" ]] && semver_at_least "$NODE_VERSION" "$MIN_NODE_VERSION"; then
|
||||
printf '%s\n' "$NODE_VERSION"
|
||||
return
|
||||
fi
|
||||
printf '%s\n' "$MIN_NODE_VERSION"
|
||||
}
|
||||
|
||||
try_link_usable_node_runtime_from_path() {
|
||||
local path_entry
|
||||
local prefix_bin
|
||||
local -a path_entries=()
|
||||
|
||||
prefix_bin="$(node_dir)/bin"
|
||||
IFS=: read -r -a path_entries <<<"$PATH"
|
||||
for path_entry in "${path_entries[@]}"; do
|
||||
if [[ -z "$path_entry" ]]; then
|
||||
path_entry="."
|
||||
fi
|
||||
if [[ "$path_entry" == "$prefix_bin" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -x "${path_entry}/node" && -x "${path_entry}/npm" ]]; then
|
||||
link_node_runtime_paths "${path_entry}/node" "${path_entry}/npm"
|
||||
if linked_node_is_usable; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
install_alpine_node() {
|
||||
local installed_version
|
||||
local required_version
|
||||
|
||||
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"start\",\"method\":\"apk\"}"
|
||||
if try_link_usable_node_runtime_from_path; then
|
||||
installed_version="$("$(node_bin)" -v 2>/dev/null || echo unknown)"
|
||||
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"ok\",\"method\":\"system\",\"version\":\"${installed_version}\"}"
|
||||
return
|
||||
fi
|
||||
|
||||
log "Installing Node via apk (Alpine Linux detected)..."
|
||||
if is_root; then
|
||||
apk add --no-cache nodejs npm
|
||||
elif has_sudo; then
|
||||
sudo apk add --no-cache nodejs npm
|
||||
else
|
||||
fail "Alpine Linux detected, but Node musl tarballs are unavailable and sudo is unavailable. Install nodejs and npm with apk, then retry."
|
||||
fi
|
||||
|
||||
if [[ -x "${APK_NODE_BIN_DIR}/node" && -x "${APK_NODE_BIN_DIR}/npm" ]]; then
|
||||
link_node_runtime_paths "${APK_NODE_BIN_DIR}/node" "${APK_NODE_BIN_DIR}/npm"
|
||||
elif ! try_link_usable_node_runtime_from_path; then
|
||||
fail "apk Node install failed. Install nodejs and npm manually, then retry."
|
||||
fi
|
||||
|
||||
if ! linked_node_is_usable; then
|
||||
installed_version="$("$(node_bin)" -v 2>/dev/null || echo unknown)"
|
||||
required_version="$(required_node_version)"
|
||||
fail "Alpine Node package must provide Node >= ${required_version} with node:sqlite; found ${installed_version}."
|
||||
fi
|
||||
|
||||
installed_version="$("$(node_bin)" -v 2>/dev/null || echo unknown)"
|
||||
emit_json "{\"event\":\"step\",\"name\":\"node\",\"status\":\"ok\",\"method\":\"apk\",\"version\":\"${installed_version}\"}"
|
||||
}
|
||||
|
||||
set_pnpm_cmd() {
|
||||
PNPM_CMD=("$@")
|
||||
}
|
||||
@@ -560,6 +746,11 @@ install_node() {
|
||||
arch="$(arch_detect)"
|
||||
dir="$(node_dir)"
|
||||
|
||||
if [[ "$os" == "linux" ]] && command -v apk >/dev/null 2>&1 && is_musl_linux; then
|
||||
install_alpine_node
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -x "$(node_bin)" ]]; then
|
||||
current_major="$("$(node_bin)" -v 2>/dev/null | tr -d 'v' | cut -d'.' -f1 || echo "")"
|
||||
if [[ -n "$current_major" && "$current_major" -ge 22 ]]; then
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -7,7 +7,7 @@ import { describe, expect, it } from "vitest";
|
||||
const SCRIPT_PATH = "scripts/install-cli.sh";
|
||||
|
||||
function runInstallCliShell(script: string, env: NodeJS.ProcessEnv = {}) {
|
||||
return spawnSync("bash", ["-c", script], {
|
||||
return spawnSync("/bin/bash", ["-c", script], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -17,6 +17,12 @@ function runInstallCliShell(script: string, env: NodeJS.ProcessEnv = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function linkRequiredShellTools(bin: string) {
|
||||
for (const tool of ["ln", "mkdir"]) {
|
||||
symlinkSync(`/bin/${tool}`, join(bin, tool));
|
||||
}
|
||||
}
|
||||
|
||||
describe("install-cli.sh", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
@@ -125,6 +131,386 @@ describe("install-cli.sh", () => {
|
||||
expect(script).toContain('activate_repo_pnpm_version "$repo_dir"');
|
||||
});
|
||||
|
||||
it("links an existing usable Alpine/musl Node runtime without sudo", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-alpine-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const prefix = join(tmp, "prefix");
|
||||
const apkLog = join(tmp, "apk.log");
|
||||
const fakeApk = join(bin, "apk");
|
||||
const fakeNode = join(bin, "node");
|
||||
const fakeNpm = join(bin, "npm");
|
||||
|
||||
mkdirSync(bin, { recursive: true });
|
||||
linkRequiredShellTools(bin);
|
||||
writeFileSync(
|
||||
fakeApk,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'printf "%s\\n" "$*" >> "$APK_LOG"',
|
||||
"exit 99",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNode,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'if [[ "${1:-}" == "-v" ]]; then',
|
||||
" printf 'v22.22.2\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "${1:-}" == "-e" ]]; then',
|
||||
" exit 0",
|
||||
"fi",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(fakeApk, 0o755);
|
||||
chmodSync(fakeNode, 0o755);
|
||||
chmodSync(fakeNpm, 0o755);
|
||||
|
||||
try {
|
||||
const result = runInstallCliShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"os_detect() { printf 'linux\\n'; }",
|
||||
"arch_detect() { printf 'x64\\n'; }",
|
||||
"is_musl_linux() { return 0; }",
|
||||
"is_root() { return 1; }",
|
||||
`PREFIX=${JSON.stringify(prefix)}`,
|
||||
`APK_NODE_BIN_DIR=${JSON.stringify(bin)}`,
|
||||
"NODE_VERSION=22.22.0",
|
||||
"install_node",
|
||||
].join("\n"),
|
||||
{
|
||||
APK_LOG: apkLog,
|
||||
PATH: bin,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).not.toContain("Installing Node via apk");
|
||||
expect(() => readFileSync(apkLog, "utf8")).toThrow();
|
||||
const nodeLink = join(prefix, "tools", "node-v22.22.0", "bin", "node");
|
||||
const npmLink = join(prefix, "tools", "node-v22.22.0", "bin", "npm");
|
||||
expect(lstatSync(nodeLink).isSymbolicLink()).toBe(true);
|
||||
expect(readlinkSync(nodeLink)).toBe(fakeNode);
|
||||
expect(readlinkSync(npmLink)).toBe(fakeNpm);
|
||||
expect(script).toContain("apk add --no-cache git");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("replaces a stale Alpine/musl prefix Node before the generic skip", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-alpine-stale-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const oldBin = join(tmp, "old-bin");
|
||||
const prefix = join(tmp, "prefix");
|
||||
const nodePrefixBin = join(prefix, "tools", "node-v22.22.0", "bin");
|
||||
const apkLog = join(tmp, "apk.log");
|
||||
const fakeApk = join(bin, "apk");
|
||||
const fakeNode = join(bin, "node");
|
||||
const fakeNpm = join(bin, "npm");
|
||||
const oldNode = join(oldBin, "node");
|
||||
const oldNpm = join(oldBin, "npm");
|
||||
const staleNode = join(nodePrefixBin, "node");
|
||||
|
||||
mkdirSync(bin, { recursive: true });
|
||||
linkRequiredShellTools(bin);
|
||||
mkdirSync(oldBin, { recursive: true });
|
||||
mkdirSync(nodePrefixBin, { recursive: true });
|
||||
writeFileSync(
|
||||
fakeApk,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'printf "%s\\n" "$*" >> "$APK_LOG"',
|
||||
"exit 99",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
staleNode,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'if [[ "${1:-}" == "-v" ]]; then',
|
||||
" printf 'v22.22.0\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "${1:-}" == "-e" ]]; then',
|
||||
" exit 1",
|
||||
"fi",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNode,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'if [[ "${1:-}" == "-v" ]]; then',
|
||||
" printf 'v22.22.2\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "${1:-}" == "-e" ]]; then',
|
||||
" exit 0",
|
||||
"fi",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
oldNode,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'if [[ "${1:-}" == "-v" ]]; then',
|
||||
" printf 'v18.20.0\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "${1:-}" == "-e" ]]; then',
|
||||
" exit 1",
|
||||
"fi",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
oldNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(fakeApk, 0o755);
|
||||
chmodSync(staleNode, 0o755);
|
||||
chmodSync(oldNode, 0o755);
|
||||
chmodSync(oldNpm, 0o755);
|
||||
chmodSync(fakeNode, 0o755);
|
||||
chmodSync(fakeNpm, 0o755);
|
||||
|
||||
try {
|
||||
const result = runInstallCliShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"os_detect() { printf 'linux\\n'; }",
|
||||
"arch_detect() { printf 'x64\\n'; }",
|
||||
"is_musl_linux() { return 0; }",
|
||||
"is_root() { return 1; }",
|
||||
`PREFIX=${JSON.stringify(prefix)}`,
|
||||
"NODE_VERSION=22.22.0",
|
||||
"NODE_VERSION_REQUESTED=1",
|
||||
"install_node",
|
||||
].join("\n"),
|
||||
{
|
||||
APK_LOG: apkLog,
|
||||
PATH: `${nodePrefixBin}:${oldBin}:${bin}`,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).not.toContain("Installing Node via apk");
|
||||
expect(() => readFileSync(apkLog, "utf8")).toThrow();
|
||||
const nodeLink = join(prefix, "tools", "node-v22.22.0", "bin", "node");
|
||||
const npmLink = join(prefix, "tools", "node-v22.22.0", "bin", "npm");
|
||||
expect(lstatSync(nodeLink).isSymbolicLink()).toBe(true);
|
||||
expect(readlinkSync(nodeLink)).toBe(fakeNode);
|
||||
expect(readlinkSync(npmLink)).toBe(fakeNpm);
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("uses apk-managed Node and Git on Alpine/musl when the existing Node is unusable", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-alpine-apk-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const prefix = join(tmp, "prefix");
|
||||
const apkLog = join(tmp, "apk.log");
|
||||
const nodeState = join(tmp, "node-state");
|
||||
const fakeApk = join(bin, "apk");
|
||||
const fakeNode = join(bin, "node");
|
||||
const fakeNpm = join(bin, "npm");
|
||||
|
||||
mkdirSync(bin, { recursive: true });
|
||||
linkRequiredShellTools(bin);
|
||||
writeFileSync(
|
||||
fakeApk,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'printf "%s\\n" "$*" >> "$APK_LOG"',
|
||||
'printf "new\\n" > "$NODE_STATE"',
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNode,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'if [[ "${1:-}" == "-v" ]]; then',
|
||||
' if [[ -f "$NODE_STATE" ]]; then',
|
||||
" printf 'v22.22.2\\n'",
|
||||
" else",
|
||||
" printf 'v18.20.0\\n'",
|
||||
" fi",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "${1:-}" == "-e" ]]; then',
|
||||
' [[ -f "$NODE_STATE" ]]',
|
||||
" exit $?",
|
||||
"fi",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(fakeApk, 0o755);
|
||||
chmodSync(fakeNode, 0o755);
|
||||
chmodSync(fakeNpm, 0o755);
|
||||
|
||||
try {
|
||||
const result = runInstallCliShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"os_detect() { printf 'linux\\n'; }",
|
||||
"arch_detect() { printf 'x64\\n'; }",
|
||||
"is_musl_linux() { return 0; }",
|
||||
"is_root() { return 0; }",
|
||||
`PREFIX=${JSON.stringify(prefix)}`,
|
||||
`APK_NODE_BIN_DIR=${JSON.stringify(bin)}`,
|
||||
"NODE_VERSION=22.22.0",
|
||||
"NODE_VERSION_REQUESTED=1",
|
||||
"install_node",
|
||||
].join("\n"),
|
||||
{
|
||||
APK_LOG: apkLog,
|
||||
NODE_STATE: nodeState,
|
||||
PATH: bin,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("Installing Node via apk");
|
||||
expect(readFileSync(apkLog, "utf8")).toContain("add --no-cache nodejs npm");
|
||||
const nodeLink = join(prefix, "tools", "node-v22.22.0", "bin", "node");
|
||||
const npmLink = join(prefix, "tools", "node-v22.22.0", "bin", "npm");
|
||||
expect(lstatSync(nodeLink).isSymbolicLink()).toBe(true);
|
||||
expect(readlinkSync(nodeLink)).toBe(fakeNode);
|
||||
expect(readlinkSync(npmLink)).toBe(fakeNpm);
|
||||
expect(script).toContain("apk add --no-cache git");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects Alpine/musl Node packages below the requested runtime floor", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-alpine-old-node-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const prefix = join(tmp, "prefix");
|
||||
const apkLog = join(tmp, "apk.log");
|
||||
const fakeApk = join(bin, "apk");
|
||||
const fakeNode = join(bin, "node");
|
||||
const fakeNpm = join(bin, "npm");
|
||||
|
||||
mkdirSync(bin, { recursive: true });
|
||||
linkRequiredShellTools(bin);
|
||||
writeFileSync(
|
||||
fakeApk,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'printf "%s\\n" "$*" >> "$APK_LOG"',
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNode,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'if [[ "${1:-}" == "-v" ]]; then',
|
||||
" printf 'v22.18.0\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "${1:-}" == "-e" ]]; then',
|
||||
" exit 0",
|
||||
"fi",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(fakeApk, 0o755);
|
||||
chmodSync(fakeNode, 0o755);
|
||||
chmodSync(fakeNpm, 0o755);
|
||||
|
||||
try {
|
||||
const result = runInstallCliShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"os_detect() { printf 'linux\\n'; }",
|
||||
"arch_detect() { printf 'x64\\n'; }",
|
||||
"is_musl_linux() { return 0; }",
|
||||
"is_root() { return 0; }",
|
||||
`PREFIX=${JSON.stringify(prefix)}`,
|
||||
`APK_NODE_BIN_DIR=${JSON.stringify(bin)}`,
|
||||
"NODE_VERSION=22.22.0",
|
||||
"NODE_VERSION_REQUESTED=1",
|
||||
"install_node",
|
||||
].join("\n"),
|
||||
{
|
||||
APK_LOG: apkLog,
|
||||
PATH: bin,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(readFileSync(apkLog, "utf8")).toContain("add --no-cache nodejs npm");
|
||||
expect(result.stdout).toContain("Alpine Node package must provide Node >= 22.22.0 with node:sqlite");
|
||||
expect(result.stdout).toContain("found v22.18.0");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("clears npm freshness filters for package installs", () => {
|
||||
expect(script).toContain('freshness_flag="--min-release-age=0"');
|
||||
expect(script).toContain('npm_raw_config_has_key "min-release-age"');
|
||||
@@ -146,7 +532,7 @@ describe("install-cli.sh", () => {
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"#!/bin/bash",
|
||||
'if [[ "$1" == "config" && "$2" == "get" ]]; then',
|
||||
' if [[ "$3" == "min-release-age" ]]; then',
|
||||
" printf 'null\\n'",
|
||||
@@ -213,7 +599,7 @@ describe("install-cli.sh", () => {
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"#!/bin/bash",
|
||||
'printf "%s\\n" "$*" >> "$NPM_FAKE_CALLS"',
|
||||
'if [[ "$1" == "config" && "$2" == "get" ]]; then',
|
||||
' if [[ "$3" == "min-release-age" ]]; then',
|
||||
@@ -290,7 +676,7 @@ describe("install-cli.sh", () => {
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"#!/bin/bash",
|
||||
'printf "%s\\n" "$*" >> "$NPM_FAKE_CALLS"',
|
||||
'if [[ "$1" == "config" && "$2" == "get" ]]; then',
|
||||
' if [[ "$3" == "min-release-age" ]]; then',
|
||||
|
||||
Reference in New Issue
Block a user