mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 16:54:59 +00:00
fix(daemon): handle versioned node@XX Homebrew formulas in Cellar resolution
Address review feedback: versioned Homebrew formulas (node@22, node@20) use keg-only paths where the stable symlink is at <prefix>/opt/<formula>/bin/node, not <prefix>/bin/node. Updated resolveStableNodePath to: 1. Try <prefix>/opt/<formula>/bin/node first (works for both default + versioned) 2. Fall back to <prefix>/bin/node for the default "node" formula 3. Return the original Cellar path if neither stable path exists Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
8950c59581
commit
163f5184b3
@@ -144,21 +144,35 @@ describe("resolvePreferredNodePath", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveStableNodePath", () => {
|
describe("resolveStableNodePath", () => {
|
||||||
it("resolves Homebrew Cellar path to stable symlink", async () => {
|
it("resolves Homebrew Cellar path to opt symlink", async () => {
|
||||||
|
mockNodePathPresent("/opt/homebrew/opt/node/bin/node");
|
||||||
|
|
||||||
|
const result = await resolveStableNodePath("/opt/homebrew/Cellar/node/25.7.0/bin/node");
|
||||||
|
expect(result).toBe("/opt/homebrew/opt/node/bin/node");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to bin symlink for default node formula", async () => {
|
||||||
mockNodePathPresent("/opt/homebrew/bin/node");
|
mockNodePathPresent("/opt/homebrew/bin/node");
|
||||||
|
|
||||||
const result = await resolveStableNodePath("/opt/homebrew/Cellar/node/25.7.0/bin/node");
|
const result = await resolveStableNodePath("/opt/homebrew/Cellar/node/25.7.0/bin/node");
|
||||||
expect(result).toBe("/opt/homebrew/bin/node");
|
expect(result).toBe("/opt/homebrew/bin/node");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves Intel Mac Cellar path to stable symlink", async () => {
|
it("resolves Intel Mac Cellar path to opt symlink", async () => {
|
||||||
mockNodePathPresent("/usr/local/bin/node");
|
mockNodePathPresent("/usr/local/opt/node/bin/node");
|
||||||
|
|
||||||
const result = await resolveStableNodePath("/usr/local/Cellar/node/25.7.0/bin/node");
|
const result = await resolveStableNodePath("/usr/local/Cellar/node/25.7.0/bin/node");
|
||||||
expect(result).toBe("/usr/local/bin/node");
|
expect(result).toBe("/usr/local/opt/node/bin/node");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns original path when symlink does not exist", async () => {
|
it("resolves versioned node@22 formula to opt symlink", async () => {
|
||||||
|
mockNodePathPresent("/opt/homebrew/opt/node@22/bin/node");
|
||||||
|
|
||||||
|
const result = await resolveStableNodePath("/opt/homebrew/Cellar/node@22/22.12.0/bin/node");
|
||||||
|
expect(result).toBe("/opt/homebrew/opt/node@22/bin/node");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns original path when no stable symlink exists", async () => {
|
||||||
fsMocks.access.mockRejectedValue(new Error("missing"));
|
fsMocks.access.mockRejectedValue(new Error("missing"));
|
||||||
|
|
||||||
const cellarPath = "/opt/homebrew/Cellar/node/25.7.0/bin/node";
|
const cellarPath = "/opt/homebrew/Cellar/node/25.7.0/bin/node";
|
||||||
@@ -181,7 +195,7 @@ describe("resolveStableNodePath", () => {
|
|||||||
describe("resolvePreferredNodePath — Homebrew Cellar", () => {
|
describe("resolvePreferredNodePath — Homebrew Cellar", () => {
|
||||||
it("resolves Cellar execPath to stable Homebrew symlink", async () => {
|
it("resolves Cellar execPath to stable Homebrew symlink", async () => {
|
||||||
const cellarNode = "/opt/homebrew/Cellar/node/25.7.0/bin/node";
|
const cellarNode = "/opt/homebrew/Cellar/node/25.7.0/bin/node";
|
||||||
const stableNode = "/opt/homebrew/bin/node";
|
const stableNode = "/opt/homebrew/opt/node/bin/node";
|
||||||
mockNodePathPresent(stableNode);
|
mockNodePathPresent(stableNode);
|
||||||
|
|
||||||
const execFile = vi.fn().mockResolvedValue({ stdout: "25.7.0\n", stderr: "" });
|
const execFile = vi.fn().mockResolvedValue({ stdout: "25.7.0\n", stderr: "" });
|
||||||
|
|||||||
@@ -156,21 +156,39 @@ export function renderSystemNodeWarning(
|
|||||||
/**
|
/**
|
||||||
* Homebrew Cellar paths (e.g. /opt/homebrew/Cellar/node/25.7.0/bin/node)
|
* Homebrew Cellar paths (e.g. /opt/homebrew/Cellar/node/25.7.0/bin/node)
|
||||||
* break when Homebrew upgrades Node and removes the old version directory.
|
* break when Homebrew upgrades Node and removes the old version directory.
|
||||||
* Resolve these to the stable Homebrew symlink path (/opt/homebrew/bin/node)
|
* Resolve these to a stable Homebrew-managed path that survives upgrades:
|
||||||
* which Homebrew updates automatically during upgrades.
|
* - Default formula "node": <prefix>/opt/node/bin/node or <prefix>/bin/node
|
||||||
|
* - Versioned formula "node@22": <prefix>/opt/node@22/bin/node (keg-only)
|
||||||
*/
|
*/
|
||||||
export async function resolveStableNodePath(nodePath: string): Promise<string> {
|
export async function resolveStableNodePath(nodePath: string): Promise<string> {
|
||||||
const cellarMatch = nodePath.match(/^(.+?)\/Cellar\/[^/]+\/[^/]+\/bin\/node$/);
|
const cellarMatch = nodePath.match(/^(.+?)\/Cellar\/([^/]+)\/[^/]+\/bin\/node$/);
|
||||||
if (!cellarMatch) {
|
if (!cellarMatch) {
|
||||||
return nodePath;
|
return nodePath;
|
||||||
}
|
}
|
||||||
const stablePath = `${cellarMatch[1]}/bin/node`;
|
const prefix = cellarMatch[1]; // e.g. /opt/homebrew
|
||||||
|
const formula = cellarMatch[2]; // e.g. "node" or "node@22"
|
||||||
|
|
||||||
|
// Try the Homebrew opt symlink first — works for both default and versioned formulas.
|
||||||
|
const optPath = `${prefix}/opt/${formula}/bin/node`;
|
||||||
try {
|
try {
|
||||||
await fs.access(stablePath);
|
await fs.access(optPath);
|
||||||
return stablePath;
|
return optPath;
|
||||||
} catch {
|
} catch {
|
||||||
return nodePath;
|
// fall through
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For the default "node" formula, also try the direct bin symlink.
|
||||||
|
if (formula === "node") {
|
||||||
|
const binPath = `${prefix}/bin/node`;
|
||||||
|
try {
|
||||||
|
await fs.access(binPath);
|
||||||
|
return binPath;
|
||||||
|
} catch {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolvePreferredNodePath(params: {
|
export async function resolvePreferredNodePath(params: {
|
||||||
|
|||||||
Reference in New Issue
Block a user