mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(docker): support Bash 3.2 in docker-setup.sh (#9441)
* fix(docker): use Bash 3.2-compatible upsert_env in docker-setup.sh * refactor(docker): simplify argument handling in write_extra_compose function * fix(docker): add bash 3.2 regression coverage (#9441) (thanks @mateusz-michalik) --------- Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram: truncate command registration to 100 entries to avoid `BOT_COMMANDS_TOO_MUCH` failures on startup. (#12356) Thanks @arosstale.
|
||||
- Telegram: match DM `allowFrom` against sender user id (fallback to chat id) and clarify pairing logs. (#12779) Thanks @liuxiaopai-ai.
|
||||
- Onboarding: QuickStart now auto-installs shell completion (prompt only in Manual).
|
||||
- Docker: make `docker-setup.sh` compatible with macOS Bash 3.2 and empty extra mounts. (#9441) Thanks @mateusz-michalik.
|
||||
- Auth: strip embedded line breaks from pasted API keys and tokens before storing/resolving credentials.
|
||||
- Web UI: make chat refresh smoothly scroll to the latest messages and suppress new-messages badge flash during manual refresh.
|
||||
- Tools/web_search: include provider-specific settings in the web search cache key, and pass `inlineCitations` for Grok. (#12419) Thanks @tmchow.
|
||||
|
||||
@@ -56,7 +56,6 @@ COMPOSE_ARGS=()
|
||||
write_extra_compose() {
|
||||
local home_volume="$1"
|
||||
shift
|
||||
local -a mounts=("$@")
|
||||
local mount
|
||||
|
||||
cat >"$EXTRA_COMPOSE_FILE" <<'YAML'
|
||||
@@ -71,7 +70,7 @@ YAML
|
||||
printf ' - %s:/home/node/.openclaw/workspace\n' "$OPENCLAW_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE"
|
||||
fi
|
||||
|
||||
for mount in "${mounts[@]}"; do
|
||||
for mount in "$@"; do
|
||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
done
|
||||
|
||||
@@ -86,7 +85,7 @@ YAML
|
||||
printf ' - %s:/home/node/.openclaw/workspace\n' "$OPENCLAW_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE"
|
||||
fi
|
||||
|
||||
for mount in "${mounts[@]}"; do
|
||||
for mount in "$@"; do
|
||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
||||
done
|
||||
|
||||
@@ -111,7 +110,12 @@ if [[ -n "$EXTRA_MOUNTS" ]]; then
|
||||
fi
|
||||
|
||||
if [[ -n "$HOME_VOLUME_NAME" || ${#VALID_MOUNTS[@]} -gt 0 ]]; then
|
||||
write_extra_compose "$HOME_VOLUME_NAME" "${VALID_MOUNTS[@]}"
|
||||
# Bash 3.2 + nounset treats "${array[@]}" on an empty array as unbound.
|
||||
if [[ ${#VALID_MOUNTS[@]} -gt 0 ]]; then
|
||||
write_extra_compose "$HOME_VOLUME_NAME" "${VALID_MOUNTS[@]}"
|
||||
else
|
||||
write_extra_compose "$HOME_VOLUME_NAME"
|
||||
fi
|
||||
COMPOSE_FILES+=("$EXTRA_COMPOSE_FILE")
|
||||
fi
|
||||
for compose_file in "${COMPOSE_FILES[@]}"; do
|
||||
@@ -129,7 +133,9 @@ upsert_env() {
|
||||
local -a keys=("$@")
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
declare -A seen=()
|
||||
# Use a delimited string instead of an associative array so the script
|
||||
# works with Bash 3.2 (macOS default) which lacks `declare -A`.
|
||||
local seen=" "
|
||||
|
||||
if [[ -f "$file" ]]; then
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
@@ -138,7 +144,7 @@ upsert_env() {
|
||||
for k in "${keys[@]}"; do
|
||||
if [[ "$key" == "$k" ]]; then
|
||||
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
|
||||
seen["$k"]=1
|
||||
seen="$seen$k "
|
||||
replaced=true
|
||||
break
|
||||
fi
|
||||
@@ -150,7 +156,7 @@ upsert_env() {
|
||||
fi
|
||||
|
||||
for k in "${keys[@]}"; do
|
||||
if [[ -z "${seen[$k]:-}" ]]; then
|
||||
if [[ "$seen" != *" $k "* ]]; then
|
||||
printf '%s=%s\n' "$k" "${!k-}" >>"$tmp"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -7,6 +7,13 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
const repoRoot = resolve(fileURLToPath(new URL(".", import.meta.url)), "..");
|
||||
|
||||
type DockerSetupSandbox = {
|
||||
rootDir: string;
|
||||
scriptPath: string;
|
||||
logPath: string;
|
||||
binDir: string;
|
||||
};
|
||||
|
||||
async function writeDockerStub(binDir: string, logPath: string) {
|
||||
const stub = `#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -31,105 +38,132 @@ exit 0
|
||||
await writeFile(logPath, "");
|
||||
}
|
||||
|
||||
async function createDockerSetupSandbox(): Promise<DockerSetupSandbox> {
|
||||
const rootDir = await mkdtemp(join(tmpdir(), "openclaw-docker-setup-"));
|
||||
const scriptPath = join(rootDir, "docker-setup.sh");
|
||||
const dockerfilePath = join(rootDir, "Dockerfile");
|
||||
const composePath = join(rootDir, "docker-compose.yml");
|
||||
const binDir = join(rootDir, "bin");
|
||||
const logPath = join(rootDir, "docker-stub.log");
|
||||
|
||||
const script = await readFile(join(repoRoot, "docker-setup.sh"), "utf8");
|
||||
await writeFile(scriptPath, script, { mode: 0o755 });
|
||||
await writeFile(dockerfilePath, "FROM scratch\n");
|
||||
await writeFile(
|
||||
composePath,
|
||||
"services:\n openclaw-gateway:\n image: noop\n openclaw-cli:\n image: noop\n",
|
||||
);
|
||||
await writeDockerStub(binDir, logPath);
|
||||
|
||||
return { rootDir, scriptPath, logPath, binDir };
|
||||
}
|
||||
|
||||
function createEnv(
|
||||
sandbox: DockerSetupSandbox,
|
||||
overrides: Record<string, string | undefined> = {},
|
||||
): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
PATH: `${sandbox.binDir}:${process.env.PATH ?? ""}`,
|
||||
DOCKER_STUB_LOG: sandbox.logPath,
|
||||
OPENCLAW_GATEWAY_TOKEN: "test-token",
|
||||
OPENCLAW_CONFIG_DIR: join(sandbox.rootDir, "config"),
|
||||
OPENCLAW_WORKSPACE_DIR: join(sandbox.rootDir, "openclaw"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("docker-setup.sh", () => {
|
||||
it("handles unset optional env vars under strict mode", async () => {
|
||||
const assocCheck = spawnSync("bash", ["-c", "declare -A _t=()"], {
|
||||
encoding: "utf8",
|
||||
const sandbox = await createDockerSetupSandbox();
|
||||
const env = createEnv(sandbox, {
|
||||
OPENCLAW_DOCKER_APT_PACKAGES: undefined,
|
||||
OPENCLAW_EXTRA_MOUNTS: undefined,
|
||||
OPENCLAW_HOME_VOLUME: undefined,
|
||||
});
|
||||
if (assocCheck.status !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootDir = await mkdtemp(join(tmpdir(), "openclaw-docker-setup-"));
|
||||
const scriptPath = join(rootDir, "docker-setup.sh");
|
||||
const dockerfilePath = join(rootDir, "Dockerfile");
|
||||
const composePath = join(rootDir, "docker-compose.yml");
|
||||
const binDir = join(rootDir, "bin");
|
||||
const logPath = join(rootDir, "docker-stub.log");
|
||||
|
||||
const script = await readFile(join(repoRoot, "docker-setup.sh"), "utf8");
|
||||
await writeFile(scriptPath, script, { mode: 0o755 });
|
||||
await writeFile(dockerfilePath, "FROM scratch\n");
|
||||
await writeFile(
|
||||
composePath,
|
||||
"services:\n openclaw-gateway:\n image: noop\n openclaw-cli:\n image: noop\n",
|
||||
);
|
||||
await writeDockerStub(binDir, logPath);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
||||
DOCKER_STUB_LOG: logPath,
|
||||
OPENCLAW_GATEWAY_TOKEN: "test-token",
|
||||
OPENCLAW_CONFIG_DIR: join(rootDir, "config"),
|
||||
OPENCLAW_WORKSPACE_DIR: join(rootDir, "openclaw"),
|
||||
};
|
||||
delete env.OPENCLAW_DOCKER_APT_PACKAGES;
|
||||
delete env.OPENCLAW_EXTRA_MOUNTS;
|
||||
delete env.OPENCLAW_HOME_VOLUME;
|
||||
|
||||
const result = spawnSync("bash", [scriptPath], {
|
||||
cwd: rootDir,
|
||||
const result = spawnSync("bash", [sandbox.scriptPath], {
|
||||
cwd: sandbox.rootDir,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
const envFile = await readFile(join(rootDir, ".env"), "utf8");
|
||||
const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8");
|
||||
expect(envFile).toContain("OPENCLAW_DOCKER_APT_PACKAGES=");
|
||||
expect(envFile).toContain("OPENCLAW_EXTRA_MOUNTS=");
|
||||
expect(envFile).toContain("OPENCLAW_HOME_VOLUME=");
|
||||
});
|
||||
|
||||
it("plumbs OPENCLAW_DOCKER_APT_PACKAGES into .env and docker build args", async () => {
|
||||
const assocCheck = spawnSync("bash", ["-c", "declare -A _t=()"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (assocCheck.status !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootDir = await mkdtemp(join(tmpdir(), "openclaw-docker-setup-"));
|
||||
const scriptPath = join(rootDir, "docker-setup.sh");
|
||||
const dockerfilePath = join(rootDir, "Dockerfile");
|
||||
const composePath = join(rootDir, "docker-compose.yml");
|
||||
const binDir = join(rootDir, "bin");
|
||||
const logPath = join(rootDir, "docker-stub.log");
|
||||
|
||||
const script = await readFile(join(repoRoot, "docker-setup.sh"), "utf8");
|
||||
await writeFile(scriptPath, script, { mode: 0o755 });
|
||||
await writeFile(dockerfilePath, "FROM scratch\n");
|
||||
await writeFile(
|
||||
composePath,
|
||||
"services:\n openclaw-gateway:\n image: noop\n openclaw-cli:\n image: noop\n",
|
||||
);
|
||||
await writeDockerStub(binDir, logPath);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
||||
DOCKER_STUB_LOG: logPath,
|
||||
OPENCLAW_DOCKER_APT_PACKAGES: "ffmpeg build-essential",
|
||||
OPENCLAW_GATEWAY_TOKEN: "test-token",
|
||||
OPENCLAW_CONFIG_DIR: join(rootDir, "config"),
|
||||
OPENCLAW_WORKSPACE_DIR: join(rootDir, "openclaw"),
|
||||
it("supports a home volume when extra mounts are empty", async () => {
|
||||
const sandbox = await createDockerSetupSandbox();
|
||||
const env = createEnv(sandbox, {
|
||||
OPENCLAW_EXTRA_MOUNTS: "",
|
||||
OPENCLAW_HOME_VOLUME: "",
|
||||
};
|
||||
OPENCLAW_HOME_VOLUME: "openclaw-home",
|
||||
});
|
||||
|
||||
const result = spawnSync("bash", [scriptPath], {
|
||||
cwd: rootDir,
|
||||
const result = spawnSync("bash", [sandbox.scriptPath], {
|
||||
cwd: sandbox.rootDir,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
const envFile = await readFile(join(rootDir, ".env"), "utf8");
|
||||
const extraCompose = await readFile(join(sandbox.rootDir, "docker-compose.extra.yml"), "utf8");
|
||||
expect(extraCompose).toContain("openclaw-home:/home/node");
|
||||
expect(extraCompose).toContain("volumes:");
|
||||
expect(extraCompose).toContain("openclaw-home:");
|
||||
});
|
||||
|
||||
it("avoids associative arrays so the script remains Bash 3.2-compatible", async () => {
|
||||
const script = await readFile(join(repoRoot, "docker-setup.sh"), "utf8");
|
||||
expect(script).not.toMatch(/^\s*declare -A\b/m);
|
||||
|
||||
const systemBash = "/bin/bash";
|
||||
const assocCheck = spawnSync(systemBash, ["-c", "declare -A _t=()"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (assocCheck.status === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sandbox = await createDockerSetupSandbox();
|
||||
const env = createEnv(sandbox, {
|
||||
OPENCLAW_EXTRA_MOUNTS: "",
|
||||
OPENCLAW_HOME_VOLUME: "",
|
||||
});
|
||||
const result = spawnSync(systemBash, [sandbox.scriptPath], {
|
||||
cwd: sandbox.rootDir,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stderr).not.toContain("declare: -A: invalid option");
|
||||
});
|
||||
|
||||
it("plumbs OPENCLAW_DOCKER_APT_PACKAGES into .env and docker build args", async () => {
|
||||
const sandbox = await createDockerSetupSandbox();
|
||||
const env = createEnv(sandbox, {
|
||||
OPENCLAW_DOCKER_APT_PACKAGES: "ffmpeg build-essential",
|
||||
OPENCLAW_EXTRA_MOUNTS: "",
|
||||
OPENCLAW_HOME_VOLUME: "",
|
||||
});
|
||||
|
||||
const result = spawnSync("bash", [sandbox.scriptPath], {
|
||||
cwd: sandbox.rootDir,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8");
|
||||
expect(envFile).toContain("OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential");
|
||||
|
||||
const log = await readFile(logPath, "utf8");
|
||||
const log = await readFile(sandbox.logPath, "utf8");
|
||||
expect(log).toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user