Skip to content

Commit d4ae13f

Browse files
authored
fix(opencode): serialize config bun installs (#17342)
1 parent f4804da commit d4ae13f

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

packages/opencode/src/config/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { Account } from "@/account"
3737
import { ConfigPaths } from "./paths"
3838
import { Filesystem } from "@/util/filesystem"
3939
import { Process } from "@/util/process"
40+
import { Lock } from "@/util/lock"
4041

4142
export namespace Config {
4243
const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
@@ -289,6 +290,7 @@ export namespace Config {
289290

290291
// Install any additional dependencies defined in the package.json
291292
// This allows local plugins and custom tools to use external packages
293+
using _ = await Lock.write("bun-install")
292294
await BunProc.run(
293295
[
294296
"install",

packages/opencode/test/config/config.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect, describe, mock, afterEach } from "bun:test"
1+
import { test, expect, describe, mock, afterEach, spyOn } from "bun:test"
22
import { Config } from "../../src/config/config"
33
import { Instance } from "../../src/project/instance"
44
import { Auth } from "../../src/auth"
@@ -10,6 +10,7 @@ import { pathToFileURL } from "url"
1010
import { Global } from "../../src/global"
1111
import { ProjectID } from "../../src/project/schema"
1212
import { Filesystem } from "../../src/util/filesystem"
13+
import { BunProc } from "../../src/bun"
1314

1415
// Get managed config directory from environment (set in preload.ts)
1516
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
@@ -763,6 +764,39 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
763764
}
764765
})
765766

767+
test("serializes concurrent config dependency installs", async () => {
768+
await using tmp = await tmpdir()
769+
const dirs = [path.join(tmp.path, "a"), path.join(tmp.path, "b")]
770+
await Promise.all(dirs.map((dir) => fs.mkdir(dir, { recursive: true })))
771+
772+
const seen: string[] = []
773+
let active = 0
774+
let max = 0
775+
const run = spyOn(BunProc, "run").mockImplementation(async (_cmd, opts) => {
776+
active++
777+
max = Math.max(max, active)
778+
seen.push(opts?.cwd ?? "")
779+
await new Promise((resolve) => setTimeout(resolve, 25))
780+
active--
781+
return {
782+
code: 0,
783+
stdout: Buffer.alloc(0),
784+
stderr: Buffer.alloc(0),
785+
}
786+
})
787+
788+
try {
789+
await Promise.all(dirs.map((dir) => Config.installDependencies(dir)))
790+
} finally {
791+
run.mockRestore()
792+
}
793+
794+
expect(max).toBe(1)
795+
expect(seen.toSorted()).toEqual(dirs.toSorted())
796+
expect(await Filesystem.exists(path.join(dirs[0], "package.json"))).toBe(true)
797+
expect(await Filesystem.exists(path.join(dirs[1], "package.json"))).toBe(true)
798+
})
799+
766800
test("resolves scoped npm plugins in config", async () => {
767801
await using tmp = await tmpdir({
768802
init: async (dir) => {

0 commit comments

Comments
 (0)