From 7609e487a8bbc3fcfd4ec84a0f35b661cbdc6eda Mon Sep 17 00:00:00 2001 From: "pullapprove5-fix[bot]" <4489445+pullapprove5-fix[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:55:04 +0000 Subject: [PATCH] Fix: Windows: os.replace onto in-use cached binary raises PermissionError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The finding is real: on Windows, os.replace(tmp_path, binary_path) in both plain-tailwind/plain/tailwind/core.py:173 and plain-code/plain/code/oxc.py:195 raises PermissionError (sharing violation) when a second checkout tries to install/download the same version binary while a first checkout is actively executing it from the shared machine-wide ~/.cache/plain cache. POSIX is unaffected because os.replace there succeeds even onto an open/executing file. I confirmed the exact behavior by exercising the real Tailwind.download() and OxcTool.download() code paths end-to-end (mocked network I/O, real os.replace monkeypatched to raise the actual Windows sharing-violation errno/message) with the destination binary pre-populated to simulate "another process already installed and is running it": on the pre-fix code this raised PermissionError; after the fix it succeeded and left the pre-existing binary in place. I fixed both call sites identically: wrap os.replace in try/except PermissionError, and only re-raise if the destination binary_path doesn't already exist (i.e. genuinely still missing, not just contended) — matching the deferred finding's suggested approach. Ran ./scripts/fix on both plain-tailwind and plain-code (after installing uv via pip since it wasn't preinstalled in this sandbox) — both passed clean with zero diff (ruff, oxlint/oxfmt, prettier all no-op). Could not run ./scripts/test: it requires a Postgres server via scripts/start-postgres, and docker isn't available in this sandbox; additionally neither plain-tailwind nor plain-code has a tests/ directory or appears in scripts/test's ALL_PACKAGES list, so the repo's own suite never covered this code before or after the change. One commit was made covering both fixes. --- plain-code/plain/code/oxc.py | 11 ++++++++++- plain-tailwind/plain/tailwind/core.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/plain-code/plain/code/oxc.py b/plain-code/plain/code/oxc.py index 9e284bdb78..35d8d33f17 100644 --- a/plain-code/plain/code/oxc.py +++ b/plain-code/plain/code/oxc.py @@ -192,7 +192,16 @@ def download(self, version: str = "") -> str: dst.write(extracted.read()) os.chmod(tmp_path, 0o755) - os.replace(tmp_path, binary_path) + try: + os.replace(tmp_path, binary_path) + except PermissionError: + # Windows raises this if another process has binary_path open + # (e.g. another checkout sharing this machine-wide cache is + # currently running it). If it's already there, another + # process finished installing this exact version — nothing + # left to do. + if not binary_path.exists(): + raise finally: tmp_path.unlink(missing_ok=True) diff --git a/plain-tailwind/plain/tailwind/core.py b/plain-tailwind/plain/tailwind/core.py index 1958267a79..ab0af401cd 100644 --- a/plain-tailwind/plain/tailwind/core.py +++ b/plain-tailwind/plain/tailwind/core.py @@ -170,7 +170,16 @@ def download(self, version: str = "") -> str: binary_path = self.binary_path(version) binary_path.parent.mkdir(parents=True, exist_ok=True) - os.replace(tmp_path, binary_path) + try: + os.replace(tmp_path, binary_path) + except PermissionError: + # Windows raises this if another process has binary_path open + # (e.g. `plain tailwind build --watch` in a different checkout + # sharing this machine-wide cache). If it's already there, + # another process finished installing this exact version — + # nothing left to do. + if not binary_path.exists(): + raise finally: tmp_path.unlink(missing_ok=True)