mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-10-31 03:55:33 +00:00 
			
		
		
		
	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.
This commit is contained in:
		
							parent
							
								
									9d3a3843c3
								
							
						
					
					
						commit
						12764df8b2
					
				
					 5 changed files with 79 additions and 3 deletions
				
			
		|  | @ -9,6 +9,7 @@ pub use settings::{BuildBackendSettings, WheelDataIncludes}; | ||||||
| pub use source_dist::{build_source_dist, list_source_dist}; | pub use source_dist::{build_source_dist, list_source_dist}; | ||||||
| pub use wheel::{build_editable, build_wheel, list_wheel, metadata}; | pub use wheel::{build_editable, build_wheel, list_wheel, metadata}; | ||||||
| 
 | 
 | ||||||
|  | use std::ffi::OsStr; | ||||||
| use std::io; | use std::io; | ||||||
| use std::path::{Path, PathBuf}; | use std::path::{Path, PathBuf}; | ||||||
| use std::str::FromStr; | use std::str::FromStr; | ||||||
|  | @ -69,6 +70,8 @@ pub enum Error { | ||||||
|     /// Either an absolute path or a parent path through `..`.
 |     /// 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())] |     #[error("The path for the data directory {} must be inside the project: `{}`", name, path.user_display())] | ||||||
|     InvalidDataRoot { name: String, path: PathBuf }, |     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}`")] |     #[error("Inconsistent metadata between prepare and build step: `{0}`")] | ||||||
|     InconsistentSteps(&'static str), |     InconsistentSteps(&'static str), | ||||||
|     #[error("Failed to write to {}", _0.user_display())] |     #[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<Pa | ||||||
|     Ok(module_relative) |     Ok(module_relative) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Error if we're adding a venv to a distribution.
 | ||||||
|  | pub(crate) fn error_on_venv(file_name: &OsStr, path: &Path) -> 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)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use super::*; |     use super::*; | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ use uv_pep508::{ | ||||||
| use uv_pypi_types::{Metadata23, VerbatimParsedUrl}; | use uv_pypi_types::{Metadata23, VerbatimParsedUrl}; | ||||||
| 
 | 
 | ||||||
| use crate::serde_verbatim::SerdeVerbatim; | use crate::serde_verbatim::SerdeVerbatim; | ||||||
| use crate::{BuildBackendSettings, Error}; | use crate::{BuildBackendSettings, Error, error_on_venv}; | ||||||
| 
 | 
 | ||||||
| /// By default, we ignore generated python files.
 | /// By default, we ignore generated python files.
 | ||||||
| pub(crate) const DEFAULT_EXCLUDES: &[&str] = &["__pycache__", "*.pyc", "*.pyo"]; | pub(crate) const DEFAULT_EXCLUDES: &[&str] = &["__pycache__", "*.pyc", "*.pyo"]; | ||||||
|  | @ -448,6 +448,8 @@ impl PyProjectToml { | ||||||
|                         continue; |                         continue; | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|  |                     error_on_venv(entry.file_name(), entry.path())?; | ||||||
|  | 
 | ||||||
|                     debug!("License files match: `{}`", relative.user_display()); |                     debug!("License files match: `{}`", relative.user_display()); | ||||||
|                     license_files.push(relative.portable_display().to_string()); |                     license_files.push(relative.portable_display().to_string()); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| use crate::metadata::DEFAULT_EXCLUDES; | use crate::metadata::DEFAULT_EXCLUDES; | ||||||
| use crate::wheel::build_exclude_matcher; | use crate::wheel::build_exclude_matcher; | ||||||
| use crate::{ | 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::Compression; | ||||||
| use flate2::write::GzEncoder; | use flate2::write::GzEncoder; | ||||||
|  | @ -266,6 +267,8 @@ fn write_source_dist( | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         error_on_venv(entry.file_name(), entry.path())?; | ||||||
|  | 
 | ||||||
|         let entry_path = Path::new(&top_level) |         let entry_path = Path::new(&top_level) | ||||||
|             .join(relative) |             .join(relative) | ||||||
|             .portable_display() |             .portable_display() | ||||||
|  |  | ||||||
|  | @ -19,7 +19,8 @@ use uv_warnings::warn_user_once; | ||||||
| 
 | 
 | ||||||
| use crate::metadata::DEFAULT_EXCLUDES; | use crate::metadata::DEFAULT_EXCLUDES; | ||||||
| use crate::{ | 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.
 | /// Build a wheel from the source tree and place it in the output directory.
 | ||||||
|  | @ -180,6 +181,8 @@ fn write_wheel( | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             error_on_venv(entry.file_name(), entry.path())?; | ||||||
|  | 
 | ||||||
|             let entry_path = entry_path.portable_display().to_string(); |             let entry_path = entry_path.portable_display().to_string(); | ||||||
|             debug!("Adding to wheel: {entry_path}"); |             debug!("Adding to wheel: {entry_path}"); | ||||||
|             wheel_writer.write_dir_entry(&entry, &entry_path)?; |             wheel_writer.write_dir_entry(&entry, &entry_path)?; | ||||||
|  | @ -529,6 +532,8 @@ fn wheel_subdir_from_globs( | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         error_on_venv(entry.file_name(), entry.path())?; | ||||||
|  | 
 | ||||||
|         let license_path = Path::new(target) |         let license_path = Path::new(target) | ||||||
|             .join(relative) |             .join(relative) | ||||||
|             .portable_display() |             .portable_display() | ||||||
|  |  | ||||||
|  | @ -987,3 +987,45 @@ fn error_on_relative_data_dir_outside_project_root() -> Result<()> { | ||||||
| 
 | 
 | ||||||
|     Ok(()) |     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 | ||||||
|  |     ");
 | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 konsti
						konsti