From 27868b9a440af26dbc36a6b78c9c3bcc79e195a9 Mon Sep 17 00:00:00 2001 From: Guillaume Pagnoux Date: Fri, 12 Sep 2025 12:30:30 +0200 Subject: [PATCH] dda: run uv from a temporary directory `UV.execution_context()` currently copies the bundled `uv` executable next to the installed binary before running it. That only works if the install directory is writable, which is not guaranteed for system-managed or otherwise protected prefixes. Copy the executable into a fresh temporary directory instead and execute it from there. This keeps the existing "run a copy of uv" behavior, which avoids modifying the in-use binary on Windows, while removing the dependency on write access to uv's install location. --- src/dda/tools/uv.py | 13 ++++---- tests/cli/inv/test_inv.py | 68 +++++++++++++++++++++------------------ tests/cli/test_dynamic.py | 36 +++++++++++---------- 3 files changed, 61 insertions(+), 56 deletions(-) diff --git a/src/dda/tools/uv.py b/src/dda/tools/uv.py index 018c60de..892068b7 100644 --- a/src/dda/tools/uv.py +++ b/src/dda/tools/uv.py @@ -39,16 +39,15 @@ def execution_context(self, command: list[str]) -> Generator[ExecutionContext, N import shutil - from dda.utils.fs import Path + from dda.utils.fs import Path, temp_directory path = Path(self.path) - safe_path = path.with_stem(f"{path.stem}-{path.id}") - shutil.copy2(self.path, safe_path) - - try: + safe_name = path.with_stem(f"{path.stem}-{path.id}").name + with temp_directory() as temp_dir: + # Always use a temporary directory to avoid permission issues. + safe_path = temp_dir / safe_name + shutil.copy2(self.path, safe_path) yield ExecutionContext(command=[str(safe_path), *command], env_vars={}) - finally: - safe_path.unlink() @cached_property def path(self) -> str | None: diff --git a/tests/cli/inv/test_inv.py b/tests/cli/inv/test_inv.py index 09652ac0..147ed23c 100644 --- a/tests/cli/inv/test_inv.py +++ b/tests/cli/inv/test_inv.py @@ -5,6 +5,7 @@ import subprocess import sys +from pathlib import Path from unittest import mock import pytest @@ -33,38 +34,41 @@ def test_default(dda, helpers, temp_dir, uv_on_path, mocker): ), ) - expected_path = str(uv_on_path.with_stem(f"{uv_on_path.stem}-{uv_on_path.id}")) - assert subprocess_run.call_args_list == [ - mock.call( - [ - expected_path, - "venv", - str(temp_dir / "data" / "venvs" / "legacy"), - "--seed", - "--python", - sys.executable, - ], - encoding="utf-8", - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ), - mock.call( - [ - expected_path, - "sync", - "--frozen", - "--no-install-project", - "--inexact", - "--only-group", - "legacy-tasks", - ], - encoding="utf-8", - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=mock.ANY, - env=mock.ANY, - ), - ] + expected_name = uv_on_path.with_stem(f"{uv_on_path.stem}-{uv_on_path.id}").name + first_call, second_call = subprocess_run.call_args_list + + assert Path(first_call.args[0][0]).name == expected_name + assert first_call == mock.call( + [ + mock.ANY, + "venv", + str(temp_dir / "data" / "venvs" / "legacy"), + "--seed", + "--python", + sys.executable, + ], + encoding="utf-8", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + assert Path(second_call.args[0][0]).name == expected_name + assert second_call == mock.call( + [ + mock.ANY, + "sync", + "--frozen", + "--no-install-project", + "--inexact", + "--only-group", + "legacy-tasks", + ], + encoding="utf-8", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=mock.ANY, + env=mock.ANY, + ) assert exit_with.call_args_list == [ mock.call( [ diff --git a/tests/cli/test_dynamic.py b/tests/cli/test_dynamic.py index 5565da0a..1ec667ec 100644 --- a/tests/cli/test_dynamic.py +++ b/tests/cli/test_dynamic.py @@ -6,6 +6,7 @@ import os import subprocess import sys +from pathlib import Path from unittest import mock @@ -108,20 +109,21 @@ def cmd(app): ), ) - expected_path = str(uv_on_path.with_stem(f"{uv_on_path.stem}-{uv_on_path.id}")) - assert subprocess_run.call_args_list == [ - mock.call( - [ - expected_path, - "pip", - "install", - "--python", - sys.executable, - "-r", - mocker.ANY, - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="utf-8", - ), - ] + expected_name = uv_on_path.with_stem(f"{uv_on_path.stem}-{uv_on_path.id}").name + (call,) = subprocess_run.call_args_list + + assert Path(call.args[0][0]).name == expected_name + assert call == mock.call( + [ + mocker.ANY, + "pip", + "install", + "--python", + sys.executable, + "-r", + mocker.ANY, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + )