diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index 054f4189e..13dfda72c 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -8,6 +8,9 @@ ### Fixed - Fixed Linux native audio over forwarded PulseAudio servers: capture now handles 125 ms Android fragments without stalling, and playback buffers enough audio to avoid TCP underruns and stuttering ([#6628](https://github.com/can1357/oh-my-pi/pull/6628) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy)). +### Fixed + +- Fixed older running OMP versions deleting newer native addon cache directories during cleanup, which could race a new version's first-run extraction and crash with `ENOENT`. ## [17.1.3] - 2026-07-24 diff --git a/packages/natives/native/loader-state.js b/packages/natives/native/loader-state.js index 2e6778c56..68bb1b159 100644 --- a/packages/natives/native/loader-state.js +++ b/packages/natives/native/loader-state.js @@ -178,6 +178,23 @@ export function resolveLoaderCandidates({ // ========================================================================= +function parseReleaseVersion(version) { + const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version); + return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null; +} + +function isOlderReleaseVersion(candidate, current) { + const candidateParts = parseReleaseVersion(candidate); + const currentParts = parseReleaseVersion(current); + if (!candidateParts || !currentParts) return false; + for (let index = 0; index < candidateParts.length; index++) { + if (candidateParts[index] !== currentParts[index]) { + return candidateParts[index] < currentParts[index]; + } + } + return false; +} + /** * Remove version-pinned native cache directories older than the loaded package. * Best-effort by design: permission errors and concurrent processes must not @@ -196,7 +213,7 @@ export function cleanupStaleNativeVersions({ nativesDir, currentVersion }) { } for (const entry of entries) { - if (!entry.isDirectory() || entry.name === currentVersion) continue; + if (!entry.isDirectory() || !isOlderReleaseVersion(entry.name, currentVersion)) continue; const targetPath = path.join(nativesDir, entry.name); try { fs.rmSync(targetPath, { recursive: true, force: true }); diff --git a/packages/natives/test/windows-staging.test.ts b/packages/natives/test/windows-staging.test.ts index 6d019a8f6..fbb577e1b 100644 --- a/packages/natives/test/windows-staging.test.ts +++ b/packages/natives/test/windows-staging.test.ts @@ -134,17 +134,23 @@ describe("windows native addon staging", () => { expect(candidates).toContain(nodeModulesBaseline); }); - it("removes stale version directories after the current native version loads", async () => { + it("removes only older version directories after the current native version loads", async () => { const nativesDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-natives-cache-")); + const currentMajor = Number.parseInt(packageJson.version, 10); + const futureVersion = `${currentMajor + 1}.0.0`; try { await fs.mkdir(path.join(nativesDir, "15.10.11")); await fs.mkdir(path.join(nativesDir, packageJson.version)); + await fs.mkdir(path.join(nativesDir, futureVersion)); + await fs.mkdir(path.join(nativesDir, "not-a-version")); await Bun.write(path.join(nativesDir, "README.txt"), "not a version directory"); const removed = cleanupStaleNativeVersions({ nativesDir, currentVersion: packageJson.version }); expect(removed.map(filePath => path.basename(filePath))).toEqual(["15.10.11"]); - expect((await fs.readdir(nativesDir)).sort()).toEqual(["README.txt", packageJson.version].sort()); + expect((await fs.readdir(nativesDir)).sort()).toEqual( + ["README.txt", packageJson.version, futureVersion, "not-a-version"].sort(), + ); } finally { await fs.rm(nativesDir, { recursive: true, force: true }); }