mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 09:01:33 +00:00
feat(runtime): run OpenClaw under Bun runtimes that provide node:sqlite (#114256)
* feat(runtime): allow Bun runtimes that provide node:sqlite * fix(process): drop execa buffer encoding under Bun spawn (Bun rejects non-spawn options) * chore(process): cite oven-sh/bun#36049 in bun spawn workaround * docs(install): bun with node:sqlite can run openclaw; bun install workspace caveat * fix(process): clear execa buffer encoding under Bun without mutating read-only options
This commit is contained in:
committed by
GitHub
parent
992a86a28f
commit
a05cfd4756
@@ -7,24 +7,20 @@ title: "Bun"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Bun cannot run the OpenClaw CLI or Gateway because it does not provide the required `node:sqlite` API. Install a supported Node version for all OpenClaw runtime commands.
|
||||
Bun releases up to 1.3.x cannot run the OpenClaw CLI or Gateway because they do not provide the required `node:sqlite` API. OpenClaw feature-probes the runtime: Bun builds that ship `node:sqlite` (1.4.0 canary and later) can run the CLI and Gateway experimentally, while older Bun versions are rejected at startup. Node remains the supported and recommended runtime for all OpenClaw runtime commands.
|
||||
</Warning>
|
||||
|
||||
Bun remains usable as an optional dependency installer and package-script runner. The default package manager remains `pnpm`, which is fully supported and used by docs tooling. Bun cannot use `pnpm-lock.yaml` and ignores it.
|
||||
Bun remains usable as an optional package-script runner. The default package manager remains `pnpm`, which is fully supported and used by docs tooling. Bun cannot use `pnpm-lock.yaml` and ignores it, and current Bun versions fail to resolve this repo's `pnpm-workspace.yaml` layout during `bun install`, so dependency installs should use `pnpm install`.
|
||||
|
||||
## Install
|
||||
|
||||
<Steps>
|
||||
<Step title="Install dependencies">
|
||||
```sh
|
||||
bun install
|
||||
pnpm install
|
||||
```
|
||||
|
||||
`bun.lock` / `bun.lockb` are gitignored, so there is no repo churn. To skip lockfile writes entirely:
|
||||
|
||||
```sh
|
||||
bun install --no-save
|
||||
```
|
||||
Current Bun versions (including 1.4 canary) cannot resolve this repo's pnpm workspace layout, so `bun install` fails during workspace resolution. Use `pnpm install`.
|
||||
|
||||
</Step>
|
||||
<Step title="Build and test">
|
||||
@@ -33,7 +29,7 @@ Bun remains usable as an optional dependency installer and package-script runner
|
||||
bun run vitest run
|
||||
```
|
||||
|
||||
Commands that launch OpenClaw itself must still run through Node.
|
||||
Commands that launch OpenClaw itself should still run through Node; Bun runtimes that provide `node:sqlite` (1.4.0 canary and later) can run them experimentally.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
13
openclaw.mjs
13
openclaw.mjs
@@ -49,8 +49,19 @@ const isSupportedNodeVersion = (version) => {
|
||||
|
||||
const ensureSupportedRuntimeVersion = () => {
|
||||
if (process.versions.bun) {
|
||||
// Bun >=1.4 (Rust rewrite) ships node:sqlite; feature-probe instead of
|
||||
// rejecting Bun outright so capable Bun builds can run OpenClaw.
|
||||
let hasNodeSqlite = false;
|
||||
try {
|
||||
hasNodeSqlite = Boolean(process.getBuiltinModule?.("node:sqlite"));
|
||||
} catch {
|
||||
hasNodeSqlite = false;
|
||||
}
|
||||
if (hasNodeSqlite) {
|
||||
return;
|
||||
}
|
||||
process.stderr.write(
|
||||
"openclaw: the Bun runtime is unsupported because OpenClaw requires node:sqlite.\n" +
|
||||
"openclaw: this Bun runtime is unsupported because it does not provide node:sqlite.\n" +
|
||||
`Use Node.js ${SUPPORTED_NODE_RANGE}; Bun remains supported for installs and package scripts.\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
|
||||
@@ -79,6 +79,7 @@ describe("runtime-guard", () => {
|
||||
version: "20.0.0",
|
||||
execPath: "/usr/bin/node",
|
||||
pathEnv: "/usr/bin",
|
||||
hasNodeSqlite: false,
|
||||
};
|
||||
expect(() => assertSupportedRuntime(runtime, details)).toThrow("exit");
|
||||
expect(runtime.error).toHaveBeenCalledOnce();
|
||||
@@ -105,12 +106,31 @@ describe("runtime-guard", () => {
|
||||
version: "22.22.3",
|
||||
execPath: "/usr/bin/node",
|
||||
pathEnv: "/usr/bin",
|
||||
hasNodeSqlite: true,
|
||||
};
|
||||
expect(assertSupportedRuntime(runtime, details)).toBeUndefined();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects Bun because it does not provide node:sqlite", () => {
|
||||
it("accepts Bun when the runtime provides node:sqlite", () => {
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
const details = {
|
||||
kind: "bun" as const,
|
||||
version: "1.4.0",
|
||||
execPath: "/usr/bin/bun",
|
||||
pathEnv: "/usr/bin",
|
||||
hasNodeSqlite: true,
|
||||
};
|
||||
expect(assertSupportedRuntime(runtime, details)).toBeUndefined();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects Bun when it does not provide node:sqlite", () => {
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
@@ -123,6 +143,7 @@ describe("runtime-guard", () => {
|
||||
version: "1.3.14",
|
||||
execPath: "/usr/bin/bun",
|
||||
pathEnv: "/usr/bin",
|
||||
hasNodeSqlite: false,
|
||||
};
|
||||
|
||||
expect(() => assertSupportedRuntime(runtime, details)).toThrow("exit");
|
||||
@@ -150,6 +171,7 @@ describe("runtime-guard", () => {
|
||||
version: null,
|
||||
execPath: null,
|
||||
pathEnv: "(not set)",
|
||||
hasNodeSqlite: false,
|
||||
};
|
||||
|
||||
expect(() => assertSupportedRuntime(runtime, details)).toThrow("exit");
|
||||
|
||||
@@ -33,6 +33,7 @@ type RuntimeDetails = {
|
||||
version: string | null;
|
||||
execPath: string | null;
|
||||
pathEnv: string;
|
||||
hasNodeSqlite: boolean;
|
||||
};
|
||||
|
||||
const SEMVER_RE = /(\d+)\.(\d+)\.(\d+)/;
|
||||
@@ -79,14 +80,28 @@ function detectRuntime(): RuntimeDetails {
|
||||
version,
|
||||
execPath: process.execPath ?? null,
|
||||
pathEnv: process.env.PATH ?? "(not set)",
|
||||
hasNodeSqlite: currentRuntimeProvidesNodeSqlite(),
|
||||
};
|
||||
}
|
||||
|
||||
// Bun >=1.4 (Rust rewrite) ships node:sqlite; older Buns do not. Feature-probe
|
||||
// instead of version-gating so the guard tracks the actual runtime capability.
|
||||
function currentRuntimeProvidesNodeSqlite(): boolean {
|
||||
try {
|
||||
return Boolean(process.getBuiltinModule?.("node:sqlite"));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether a detected runtime meets OpenClaw's minimum runtime contract. */
|
||||
function runtimeSatisfies(details: RuntimeDetails): boolean {
|
||||
if (details.kind === "node") {
|
||||
return isSupportedNodeVersion(details.version);
|
||||
}
|
||||
if (details.kind === "bun") {
|
||||
return details.hasNodeSqlite;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,14 @@ export function spawnCommandWithInvocation<
|
||||
invocation: ReturnType<typeof resolveSafeChildProcessInvocation>;
|
||||
} {
|
||||
const { baseEnv, env, windowsVerbatimArguments, ...execaOptions } = options;
|
||||
// Bun workaround (oven-sh/bun#36049): execa forwards its whole options object to
|
||||
// child_process.spawn. Node ignores the extra `encoding` key there, but Bun's
|
||||
// spawn feeds it into its stream constructor and throws ERR_UNKNOWN_ENCODING for
|
||||
// execa's "buffer". Clearing it under Bun means buffered results arrive as utf8
|
||||
// strings instead of Buffers; callers already normalize via Buffer.from. Binary-
|
||||
// exact output paths are unverified under Bun. Remove once the Bun issue is fixed.
|
||||
const dropBufferEncodingForBun =
|
||||
Boolean(process.versions.bun) && execaOptions.encoding === "buffer";
|
||||
const commandEnv = resolveCommandEnv({ argv, baseEnv, env });
|
||||
const invocation = resolveSafeChildProcessInvocation({
|
||||
argv,
|
||||
@@ -78,6 +86,7 @@ export function spawnCommandWithInvocation<
|
||||
});
|
||||
const child = execa(invocation.command, invocation.args, {
|
||||
...execaOptions,
|
||||
...(dropBufferEncodingForBun ? { encoding: undefined } : {}),
|
||||
env: commandEnv,
|
||||
extendEnv: false,
|
||||
shell: false,
|
||||
|
||||
@@ -228,7 +228,7 @@ describe("openclaw launcher", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects Bun even when its Node compatibility version is new enough", async () => {
|
||||
it("rejects Bun without node:sqlite even when its Node compatibility version is new enough", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
@@ -241,6 +241,11 @@ describe("openclaw launcher", () => {
|
||||
[
|
||||
"Object.defineProperty(process.versions, 'bun', { value: '1.3.14' });",
|
||||
"Object.defineProperty(process.versions, 'node', { value: '24.3.0' });",
|
||||
// Old Bun has no node:sqlite; the launcher feature-probes instead of
|
||||
// trusting the runtime label, so the mock must hide Node's builtin.
|
||||
"const realGetBuiltinModule = process.getBuiltinModule.bind(process);",
|
||||
"process.getBuiltinModule = (id) =>",
|
||||
" id === 'node:sqlite' || id === 'sqlite' ? undefined : realGetBuiltinModule(id);",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
@@ -258,10 +263,40 @@ describe("openclaw launcher", () => {
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(
|
||||
"the Bun runtime is unsupported because OpenClaw requires node:sqlite",
|
||||
"this Bun runtime is unsupported because it does not provide node:sqlite",
|
||||
);
|
||||
});
|
||||
|
||||
it("runs the CLI under Bun when the runtime provides node:sqlite", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
'process.stdout.write("bun-runtime-entry\\n");\n',
|
||||
"utf8",
|
||||
);
|
||||
const mockRuntime = path.join(fixtureRoot, "mock-bun-sqlite-runtime.mjs");
|
||||
await fs.writeFile(
|
||||
mockRuntime,
|
||||
// Simulates Bun >=1.4 (Rust rewrite): bun-branded runtime with node:sqlite
|
||||
// available; Node's own getBuiltinModule answers the launcher probe.
|
||||
"Object.defineProperty(process.versions, 'bun', { value: '1.4.0' });",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["--import", pathToFileURL(mockRuntime).href, path.join(fixtureRoot, "openclaw.mjs")],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("bun-runtime-entry");
|
||||
});
|
||||
|
||||
it("surfaces transitive entry import failures instead of masking them as missing dist", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await fs.writeFile(
|
||||
@@ -418,30 +453,41 @@ describe("openclaw launcher", () => {
|
||||
});
|
||||
|
||||
it.runIf(process.env.OPENCLAW_TEST_BUN_LAUNCHER === "1" && hasBunRuntime())(
|
||||
"rejects the real Bun runtime before loading the CLI",
|
||||
"gates the real Bun runtime on node:sqlite availability",
|
||||
async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
"process.stdout.write('unexpected bun entry\\n');\n",
|
||||
"process.stdout.write('bun entry ran\\n');\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.env.BUN_BIN ?? "bun",
|
||||
[path.join(fixtureRoot, "openclaw.mjs")],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
const bunBin = process.env.BUN_BIN ?? "bun";
|
||||
// Bun >=1.4 (Rust rewrite) ships node:sqlite and may run the CLI; older
|
||||
// Buns must be rejected before the entry loads.
|
||||
const probe = spawnSync(
|
||||
bunBin,
|
||||
["-e", "process.exit(process.getBuiltinModule?.('node:sqlite') ? 0 : 1)"],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
const bunHasNodeSqlite = probe.status === 0;
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(
|
||||
"the Bun runtime is unsupported because OpenClaw requires node:sqlite",
|
||||
);
|
||||
const result = spawnSync(bunBin, [path.join(fixtureRoot, "openclaw.mjs")], {
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(),
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
if (bunHasNodeSqlite) {
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("bun entry ran");
|
||||
} else {
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(
|
||||
"this Bun runtime is unsupported because it does not provide node:sqlite",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user