From 469a758c9c61b896de62da36e57fe8a632907ba4 Mon Sep 17 00:00:00 2001 From: Brad Heller Date: Mon, 6 Jul 2026 08:33:25 +0100 Subject: [PATCH 1/2] fix(tower-uv): use copy link mode when no cache dir is set When no explicit cache dir is configured, uv uses its default cache (~/.cache/uv). In containerized or sandboxed execution that default often lives on a different filesystem than the target venv, so uv can't hardlink across the two (EXDEV) and prints a noisy "Failed to hardlink files; falling back to full copy" warning into the setup logs. Force UV_LINK_MODE=copy on the sync/pip/run commands in that case so the warning is suppressed (uv was already falling back to copy anyway). Callers that pass an explicit cache dir on the same filesystem as the venv are unaffected and keep fast hardlink/clone. --- crates/tower-uv/src/lib.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/tower-uv/src/lib.rs b/crates/tower-uv/src/lib.rs index de35cedc..1d39f606 100644 --- a/crates/tower-uv/src/lib.rs +++ b/crates/tower-uv/src/lib.rs @@ -302,6 +302,20 @@ impl Uv { } } + // When no explicit cache dir is configured, uv uses its default cache + // (`~/.cache/uv`). In containerized/sandboxed execution that default often + // lives on a different filesystem than the target venv, so hardlinks across + // the two fail with EXDEV and uv prints a "Failed to hardlink files; falling + // back to full copy" warning into the setup logs. Force copy mode there so + // the warning is suppressed (uv was already copying anyway). Callers that + // pass an explicit cache dir on the same filesystem as the venv keep fast + // hardlink/clone. + fn apply_link_mode(&self, cmd: &mut Command) { + if self.cache_dir.is_none() { + cmd.env("UV_LINK_MODE", "copy"); + } + } + pub async fn venv( &self, cwd: &PathBuf, @@ -361,6 +375,7 @@ impl Uv { if let Some(dir) = &self.cache_dir { cmd.arg("--cache-dir").arg(dir); } + self.apply_link_mode(&mut cmd); let child = cmd.spawn()?; @@ -406,6 +421,7 @@ impl Uv { .arg("never") .arg("pip") .arg("install"); + self.apply_link_mode(&mut cmd); cmd } @@ -494,6 +510,8 @@ impl Uv { // Need to do this after env_clear intentionally. cmd.envs(env_vars); + // Also after env_clear so protected mode doesn't wipe it. + self.apply_link_mode(&mut cmd); if let Some(dir) = &self.cache_dir { cmd.arg("--cache-dir").arg(dir); From 58f349d7ff7cc0b28d01d0e142b2f4c53c199298 Mon Sep 17 00:00:00 2001 From: Brad Heller Date: Mon, 6 Jul 2026 08:35:44 +0100 Subject: [PATCH 2/2] docs(tower-uv): simplify apply_link_mode comment --- crates/tower-uv/src/lib.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/tower-uv/src/lib.rs b/crates/tower-uv/src/lib.rs index 1d39f606..7af6006c 100644 --- a/crates/tower-uv/src/lib.rs +++ b/crates/tower-uv/src/lib.rs @@ -302,14 +302,10 @@ impl Uv { } } - // When no explicit cache dir is configured, uv uses its default cache - // (`~/.cache/uv`). In containerized/sandboxed execution that default often - // lives on a different filesystem than the target venv, so hardlinks across - // the two fail with EXDEV and uv prints a "Failed to hardlink files; falling - // back to full copy" warning into the setup logs. Force copy mode there so - // the warning is suppressed (uv was already copying anyway). Callers that - // pass an explicit cache dir on the same filesystem as the venv keep fast - // hardlink/clone. + // Without an explicit cache dir, uv's default cache may be on a different + // filesystem than the target venv, so hardlinking fails and uv warns before + // falling back to a copy. Force copy mode to skip the failed attempt and the + // warning. fn apply_link_mode(&self, cmd: &mut Command) { if self.cache_dir.is_none() { cmd.env("UV_LINK_MODE", "copy");