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:
konsti 2025-09-09 14:41:58 +02:00 committed by GitHub
parent 9d3a3843c3
commit 12764df8b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 79 additions and 3 deletions

View file

@ -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<Pa
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)]
mod tests {
use super::*;

View file

@ -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());
}

View file

@ -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()

View file

@ -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()

View file

@ -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
");
}