From 12764df8b22947f8cacdc6da41c47a10e2ecf418 Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 9 Sep 2025 14:41:58 +0200 Subject: [PATCH] Show a dedicated error for venvs in source trees (#15748) A user in the support chat had an error message for `uv build` with the `uv_build` backend they didn't understand, which was caused by them having a venv in their build directory. This PR adds a dedicated error message when adding something to a distribution that looks like a venv. --- crates/uv-build-backend/src/lib.rs | 24 +++++++++++++ crates/uv-build-backend/src/metadata.rs | 4 ++- crates/uv-build-backend/src/source_dist.rs | 5 ++- crates/uv-build-backend/src/wheel.rs | 7 +++- crates/uv/tests/it/build_backend.rs | 42 ++++++++++++++++++++++ 5 files changed, 79 insertions(+), 3 deletions(-) diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 683feb486..154b6e26c 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -9,6 +9,7 @@ pub use settings::{BuildBackendSettings, WheelDataIncludes}; pub use source_dist::{build_source_dist, list_source_dist}; pub use wheel::{build_editable, build_wheel, list_wheel, metadata}; +use std::ffi::OsStr; use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -69,6 +70,8 @@ pub enum Error { /// Either an absolute path or a parent path through `..`. #[error("The path for the data directory {} must be inside the project: `{}`", name, path.user_display())] InvalidDataRoot { name: String, path: PathBuf }, + #[error("Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: {}", _0.user_display())] + VenvInSourceTree(PathBuf), #[error("Inconsistent metadata between prepare and build step: `{0}`")] InconsistentSteps(&'static str), #[error("Failed to write to {}", _0.user_display())] @@ -352,6 +355,27 @@ fn module_path_from_module_name(src_root: &Path, module_name: &str) -> Result Result<(), Error> { + // On 64-bit Unix, `lib64` is a (compatibility) symlink to lib. If we traverse `lib64` before + // `pyvenv.cfg`, we show a generic error for symlink directories instead. + if !(file_name == "pyvenv.cfg" || file_name == "lib64") { + return Ok(()); + } + + let Some(parent) = path.parent() else { + return Ok(()); + }; + + if parent.join("bin").join("python").is_symlink() + || parent.join("Scripts").join("python.exe").is_file() + { + return Err(Error::VenvInSourceTree(parent.to_path_buf())); + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index 1d0f758ad..52e294640 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -21,7 +21,7 @@ use uv_pep508::{ use uv_pypi_types::{Metadata23, VerbatimParsedUrl}; use crate::serde_verbatim::SerdeVerbatim; -use crate::{BuildBackendSettings, Error}; +use crate::{BuildBackendSettings, Error, error_on_venv}; /// By default, we ignore generated python files. pub(crate) const DEFAULT_EXCLUDES: &[&str] = &["__pycache__", "*.pyc", "*.pyo"]; @@ -448,6 +448,8 @@ impl PyProjectToml { continue; } + error_on_venv(entry.file_name(), entry.path())?; + debug!("License files match: `{}`", relative.user_display()); license_files.push(relative.portable_display().to_string()); } diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index 81eaea5e1..bbdecb2e9 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -1,7 +1,8 @@ use crate::metadata::DEFAULT_EXCLUDES; use crate::wheel::build_exclude_matcher; use crate::{ - BuildBackendSettings, DirectoryWriter, Error, FileList, ListWriter, PyProjectToml, find_roots, + BuildBackendSettings, DirectoryWriter, Error, FileList, ListWriter, PyProjectToml, + error_on_venv, find_roots, }; use flate2::Compression; use flate2::write::GzEncoder; @@ -266,6 +267,8 @@ fn write_source_dist( continue; } + error_on_venv(entry.file_name(), entry.path())?; + let entry_path = Path::new(&top_level) .join(relative) .portable_display() diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index 762424a26..1ce717ad5 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -19,7 +19,8 @@ use uv_warnings::warn_user_once; use crate::metadata::DEFAULT_EXCLUDES; use crate::{ - BuildBackendSettings, DirectoryWriter, Error, FileList, ListWriter, PyProjectToml, find_roots, + BuildBackendSettings, DirectoryWriter, Error, FileList, ListWriter, PyProjectToml, + error_on_venv, find_roots, }; /// Build a wheel from the source tree and place it in the output directory. @@ -180,6 +181,8 @@ fn write_wheel( continue; } + error_on_venv(entry.file_name(), entry.path())?; + let entry_path = entry_path.portable_display().to_string(); debug!("Adding to wheel: {entry_path}"); wheel_writer.write_dir_entry(&entry, &entry_path)?; @@ -529,6 +532,8 @@ fn wheel_subdir_from_globs( continue; } + error_on_venv(entry.file_name(), entry.path())?; + let license_path = Path::new(target) .join(relative) .portable_display() diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index 350f3a522..9b7d6a4c6 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -987,3 +987,45 @@ fn error_on_relative_data_dir_outside_project_root() -> Result<()> { Ok(()) } + +/// Show an explicit error when there is a venv in source tree. +#[test] +fn venv_in_source_tree() { + let context = TestContext::new("3.12"); + + context + .init() + .arg("--lib") + .arg("--name") + .arg("foo") + .assert() + .success(); + + context + .venv() + .arg(context.temp_dir.join("src").join("foo").join(".venv")) + .assert() + .success(); + + uv_snapshot!(context.filters(), context.build(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + × Failed to build `[TEMP_DIR]/` + ╰─▶ Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: src/foo/.venv + "); + + uv_snapshot!(context.filters(), context.build().arg("--wheel"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building wheel (uv build backend)... + × Failed to build `[TEMP_DIR]/` + ╰─▶ Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: src/foo/.venv + "); +}