Skip to content

Windows: parallel bun install --no-cache with shared BUN_INSTALL_CACHE_DIR can fail with ENOENT opening cache/package/version dir #28062

@Hona

Description

@Hona

What version of Bun is running?

1.3.10+30e609e08

What platform is your computer?

Microsoft Windows NT 10.0.22631.0 x64

What steps can reproduce the bug?

Two fresh project dirs, same package.json, same fresh BUN_INSTALL_CACHE_DIR, and run bun install --no-cache --verbose in parallel.

Serial with the same shared cache passes. Parallel with the same shared cache fails.

Minimal repro:

import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"

const bun = process.execPath
const root = await mkdtemp(path.join(tmpdir(), "bun-install-race-"))
const cache = path.join(root, "cache")
const pkg = JSON.stringify(
  {
    name: "bun-install-race",
    private: true,
    packageManager: "bun@1.3.10",
    dependencies: {
      "@opencode-ai/plugin": "1.2.25",
    },
  },
  null,
  2,
)

const write = async (file: string, content: string) => {
  await mkdir(path.dirname(file), { recursive: true })
  await writeFile(file, content)
}

const setup = async (dir: string) => {
  await write(path.join(dir, "package.json"), pkg)
}

const reset = async (dir: string) => {
  await rm(path.join(dir, "node_modules"), { recursive: true, force: true })
  await rm(path.join(dir, "bun.lock"), { force: true })
  await rm(path.join(dir, "package-lock.json"), { force: true })
}

const run = async (cwd: string) => {
  const proc = Bun.spawn([bun, "install", "--no-cache", "--verbose"], {
    cwd,
    stdout: "pipe",
    stderr: "pipe",
    env: {
      ...process.env,
      BUN_BE_BUN: "1",
      BUN_INSTALL_CACHE_DIR: cache,
    },
  })
  const [code, stdout, stderr] = await Promise.all([
    proc.exited,
    new Response(proc.stdout).text(),
    new Response(proc.stderr).text(),
  ])
  return { code, stdout, stderr }
}

const a = path.join(root, "a")
const b = path.join(root, "b")
await setup(a)
await setup(b)

await reset(a)
await reset(b)
await rm(cache, { recursive: true, force: true })
console.log("serial")
console.log((await run(a)).code, (await run(b)).code)

await reset(a)
await reset(b)
await rm(cache, { recursive: true, force: true })
console.log("parallel")
console.log(await Promise.all([run(a), run(b)]))

The exact command that reproduced it here was:

bun tmp/bun-install-race.ts 3

What is the expected behavior?

Both installs should succeed. --no-cache should not let concurrent installs invalidate a shared package cache entry another install is about to use.

What do you see instead?

One of the parallel installs can fail with:

ENOENT: failed opening cache/package/version dir for package @opencode-ai/sdk

or:

ENOENT: failed opening cache/package/version dir for package zod

Observed pattern here:

  • serial with shared cache: passes
  • parallel with shared cache: fails
  • no lockfiles needed for the minimal case

Additional information

Likely Windows-only cache publish race.

  • --no-cache only disables manifest cache, not the package/install cache: src/install/PackageManager/PackageManagerOptions.zig:534
  • shared cache dir is still used normally: src/install/PackageManager/PackageManagerDirectories.zig:121
  • Windows cache publish path is in src/install/extract_tarball.zig:354
  • cache path is later resolved via src/install/PackageManager/PackageManagerDirectories.zig:431
  • install then opens the real cache dir in src/install/PackageInstall.zig:506

The suspicious bit is the Windows collision handling in extract_tarball.zig: if another process already created the destination cache dir, Bun currently tries to move/delete/replace that existing cache entry. Another concurrent install can already have resolved the version link and then hit ENOENT when opening the real cache dir.

Locally, a self-contained regression test built around fixture packages reproduces this 10/10 on 1.3.10 and points at the same code path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions