From e90acaea674f5f7f9f17ea472104afdc1422ada6 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:10:52 +0800 Subject: [PATCH] fix(update): resolve Bun global directories before changing cwd --- src/application/Update.test.ts | 24 ++++++++---- src/application/UpdateRuntime.ts | 8 ++-- .../process-global/updateMaintenance.test.ts | 38 ++++++++++++------- 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/application/Update.test.ts b/src/application/Update.test.ts index 06f7455bc..26c580ab0 100644 --- a/src/application/Update.test.ts +++ b/src/application/Update.test.ts @@ -1,4 +1,4 @@ -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; @@ -413,17 +413,17 @@ describe("Bun installation ownership", () => { it("follows Bun's global directory precedence", () => { const home = { HOME: "/home/user" }; expect(bunGlobalDirectory(home, "linux")).toBe( - join("/home/user", ".bun", "install", "global"), + resolve("/home/user", ".bun", "install", "global"), ); expect( bunGlobalDirectory({ ...home, XDG_CACHE_HOME: "/cache" }, "linux"), - ).toBe(join("/cache", ".bun", "install", "global")); + ).toBe(resolve("/cache", ".bun", "install", "global")); expect( bunGlobalDirectory( { ...home, XDG_CACHE_HOME: "/cache", BUN_INSTALL: "/opt/bun" }, "linux", ), - ).toBe(join("/opt/bun", "install", "global")); + ).toBe(resolve("/opt/bun", "install", "global")); expect( bunGlobalDirectory( { @@ -433,12 +433,22 @@ describe("Bun installation ownership", () => { }, "linux", ), - ).toBe("/srv/bun-global"); + ).toBe(resolve("/srv/bun-global")); + const profile = resolve("profile"); expect( bunGlobalDirectory( - { USERPROFILE: "C:\\Users\\user", HOME: "/c/Users/user" }, + { USERPROFILE: profile, HOME: resolve("other-home") }, "win32", ), - ).toBe(join("C:\\Users\\user", ".bun", "install", "global")); + ).toBe(join(profile, ".bun", "install", "global")); + }); + + it("resolves relative installation and cache roots before appending the global layout", () => { + expect(bunGlobalDirectory({ BUN_INSTALL: "bun" }, "linux")).toBe( + resolve("bun", "install", "global"), + ); + expect(bunGlobalDirectory({ XDG_CACHE_HOME: "cache" }, "linux")).toBe( + resolve("cache", ".bun", "install", "global"), + ); }); }); diff --git a/src/application/UpdateRuntime.ts b/src/application/UpdateRuntime.ts index ea8a25bd2..7325a4142 100644 --- a/src/application/UpdateRuntime.ts +++ b/src/application/UpdateRuntime.ts @@ -141,19 +141,21 @@ const nonemptyEnvironment = (value: string | undefined): string | undefined => * Bun's global project directory for the selected environment, in Bun's precedence: * BUN_INSTALL_GLOBAL_DIR, then $BUN_INSTALL/install/global, then * $XDG_CACHE_HOME/.bun/install/global, then ~/.bun/install/global. + * Resolve against the caller's cwd before reusing the path as a subprocess cwd. */ export const bunGlobalDirectory = ( environment: Readonly, platform: NodeJS.Platform, ): string => { const explicit = nonemptyEnvironment(environment.BUN_INSTALL_GLOBAL_DIR); - if (explicit !== undefined) return explicit; + if (explicit !== undefined) return resolve(explicit); const installRoot = nonemptyEnvironment(environment.BUN_INSTALL); - if (installRoot !== undefined) return join(installRoot, "install", "global"); + if (installRoot !== undefined) + return resolve(installRoot, "install", "global"); const cacheRoot = nonemptyEnvironment(environment.XDG_CACHE_HOME) ?? homeDirectoryFromEnvironment(environment, platform); - return join(cacheRoot, ".bun", "install", "global"); + return resolve(cacheRoot, ".bun", "install", "global"); }; /** diff --git a/tests/process-global/updateMaintenance.test.ts b/tests/process-global/updateMaintenance.test.ts index 467ac1a4e..a60a2e213 100644 --- a/tests/process-global/updateMaintenance.test.ts +++ b/tests/process-global/updateMaintenance.test.ts @@ -1,9 +1,9 @@ import { mkdir, readFile, rm, writeFile, access } from "node:fs/promises"; -import { delimiter, dirname, join, resolve } from "node:path"; +import { delimiter, dirname, join, relative, resolve } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { err, ok } from "../../src/domain/result.js"; -import { runUpdate } from "../../src/application/Update.js"; +import { runUpdate, type UpdateResult } from "../../src/application/Update.js"; import { PRODUCT_IDENTITY } from "../../src/identity.js"; import { supportedClients } from "../../src/application/SupportedClients.js"; import { @@ -375,14 +375,15 @@ describe("update subprocess boundaries", () => { describe("Bun global installation update", () => { it.each([ - ["1.4.2", "updated"], - ["1.2.17", "updated"], - ["1.2.14", "failed"], - ["1.2.17-canary.1", "failed"], - ["unknown", "failed"], + ["1.4.2", "updated", "absolute"], + ["1.2.17", "updated", "absolute"], + ["1.2.17", "updated", "relative"], + ["1.2.14", "failed", "absolute"], + ["1.2.17-canary.1", "failed", "absolute"], + ["unknown", "failed", "absolute"], ])( - "handles Bun %s with outcome %s inside its global project", - async (bunVersion, status) => { + "handles Bun %s with outcome %s using %s global-directory configuration", + async (bunVersion, status, directoryMode) => { // A recording `bun` on PATH crosses the real subprocess boundary without registry access. const root = await createTestTempDirectory("rea-update-bun-"); const globalDirectory = join(root, "bun", "install", "global"); @@ -440,12 +441,21 @@ describe("Bun global installation update", () => { BUN_INSTALL: join(root, "bun"), }; delete environment.BUN_INSTALL_GLOBAL_DIR; + if (directoryMode === "relative") + environment.BUN_INSTALL_GLOBAL_DIR = relative(root, globalDirectory); - const result = await runUpdate( - "3.2.1", - systemUpdateHost(packageRoot, home, environment), - "structured", - ); + const callerDirectory = process.cwd(); + let result: UpdateResult; + try { + process.chdir(root); + result = await runUpdate( + "3.2.1", + systemUpdateHost(packageRoot, home, environment), + "structured", + ); + } finally { + process.chdir(callerDirectory); + } const invocations = (await readFile(logPath, "utf8")) .trim()