Skip to content

Commit ea9e08a

Browse files
NiveditJainclaude
andauthored
[luv-27] Fall back to docker rm for root-owned files in luv --clean (#27)
A previous fix made _force_rmtree chmod-and-retry, but Docker bind-mounts can leave files owned by root (e.g. Rust target-docker caches built inside dev-environment). nivedit can't chmod those — os.chmod raises EPERM. On PermissionError, run a busybox container with the workspace's parent dir bind-mounted and have its root rm -rf the workspace. Symmetric with how Docker created the mess. Cron-friendly: no sudo, no root crontab. No-op if docker isn't installed. Bump to 0.0.20. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8542b91 commit ea9e08a

2 files changed

Lines changed: 29 additions & 3 deletions

File tree

‎luv/__init__.py‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,10 +411,36 @@ def _on_rm_error(func, path, _exc):
411411
func(path)
412412

413413

414+
def _docker_wipe(path: Path) -> bool:
415+
"""rm -rf `path` from inside a busybox container so the container's root can
416+
delete files that a previous container bind-mounted in as root (e.g. Rust
417+
target dirs built inside the workspace's dev-environment). Returns True on
418+
success. No-op if docker isn't available."""
419+
if shutil.which("docker") is None:
420+
return False
421+
parent = path.resolve().parent
422+
name = path.name
423+
r = subprocess.run(
424+
["docker", "run", "--rm",
425+
"-v", f"{parent}:/p",
426+
"busybox", "rm", "-rf", f"/p/{name}"],
427+
capture_output=True, text=True,
428+
)
429+
if r.returncode != 0:
430+
sys.stderr.write(f"luv: docker rm fallback failed for {path}: {r.stderr.strip()}\n")
431+
return False
432+
return not path.exists()
433+
434+
414435
def _force_rmtree(path: Path) -> None:
415-
"""rmtree that chmods read-only files (e.g. uv's CACHEDIR.TAG) instead of crashing."""
436+
"""rmtree that survives read-only files (chmod-and-retry) and root-owned
437+
files left behind by Docker bind-mounts (containerized rm -rf fallback)."""
416438
kwargs = {"onexc": _on_rm_error} if sys.version_info >= (3, 12) else {"onerror": _on_rm_error}
417-
shutil.rmtree(path, **kwargs)
439+
try:
440+
shutil.rmtree(path, **kwargs)
441+
except PermissionError:
442+
if not _docker_wipe(path):
443+
raise
418444

419445

420446
def cmd_clean(force: bool = False, safe: bool = False) -> None:

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "luv-cli"
7-
version = "0.0.19"
7+
version = "0.0.20"
88
description = "Launch Claude Code agents on GitHub repos with isolated workspaces and optional Docker dev environments"
99
requires-python = ">=3.10"
1010
license = "MIT"

0 commit comments

Comments
 (0)