Noisily allow redundant entries in `tool.uv.build-backend.module-name` (#16928)

## Summary

Fix #16906 by pruning modules or submodules which are already included
(either directly, or through a parent).

Generates warnings when this happens.

Example:

```bash session
$ uv build
Building source distribution (uv build backend)...
warning: Ignoring redundant module name(s): test_lib.bar test_lib test_lib.bar.baz test_lib.baz
Building wheel from source distribution (uv build backend)...
Successfully built dist/test-0.1.0.tar.gz
Successfully built dist/test-0.1.0-py3-none-any.whl
```

## Test Plan

Added some unit tests for the pruning function and one for the whole
build backend. Added an integration test for the warnings. Ran the full
test suite. Manually tested.

The unit test for the function doesn't cater for the fact that it
doesn't guarantee an order at the moment. I think this is fine.

---------

Co-authored-by: konsti <konstin@mailbox.org>
This commit is contained in:
Tomasz Kramkowski 2025-12-03 10:05:28 +00:00 committed by GitHub
parent ed63be5dab
commit f01366bae8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 304 additions and 18 deletions

View File

@ -1,3 +1,4 @@
use itertools::Itertools;
mod metadata; mod metadata;
mod serde_verbatim; mod serde_verbatim;
mod settings; mod settings;
@ -7,8 +8,10 @@ mod wheel;
pub use metadata::{PyProjectToml, check_direct_build}; pub use metadata::{PyProjectToml, check_direct_build};
pub use settings::{BuildBackendSettings, WheelDataIncludes}; 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};
use uv_warnings::warn_user_once;
pub use wheel::{build_editable, build_wheel, list_wheel, metadata}; pub use wheel::{build_editable, build_wheel, list_wheel, metadata};
use std::collections::HashSet;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -191,6 +194,60 @@ fn check_metadata_directory(
Ok(()) Ok(())
} }
/// Returns the list of module names without names which would be included twice
///
/// In normal cases it should do nothing:
///
/// * `["aaa"] -> ["aaa"]`
/// * `["aaa", "bbb"] -> ["aaa", "bbb"]`
///
/// Duplicate elements are removed:
///
/// * `["aaa", "aaa"] -> ["aaa"]`
/// * `["bbb", "aaa", "bbb"] -> ["aaa", "bbb"]`
///
/// Names with more specific paths are removed in favour of more general paths:
///
/// * `["aaa.foo", "aaa"] -> ["aaa"]`
/// * `["bbb", "aaa", "bbb.foo", "ccc.foo", "ccc.foo.bar", "aaa"] -> ["aaa", "bbb.foo", "ccc.foo"]`
///
/// This does not preserve the order of the elements.
fn prune_redundant_modules(mut names: Vec<String>) -> Vec<String> {
names.sort();
let mut pruned = Vec::with_capacity(names.len());
for name in names {
if let Some(last) = pruned.last() {
if name == *last {
continue;
}
// This is a more specific (narrow) module name than what came before
if name
.strip_prefix(last)
.is_some_and(|suffix| suffix.starts_with('.'))
{
continue;
}
}
pruned.push(name);
}
pruned
}
/// Wraps [`prune_redundant_modules`] with a conditional warning when modules are ignored
fn prune_redundant_modules_warn(names: &[String], show_warnings: bool) -> Vec<String> {
let pruned = prune_redundant_modules(names.to_vec());
if show_warnings && names.len() != pruned.len() {
let mut pruned: HashSet<_> = pruned.iter().collect();
let ignored: Vec<_> = names.iter().filter(|name| !pruned.remove(name)).collect();
let s = if ignored.len() == 1 { "" } else { "s" };
warn_user_once!(
"Ignoring redundant module name{s} in `tool.uv.build-backend.module-name`: `{}`",
ignored.into_iter().join("`, `")
);
}
pruned
}
/// Returns the source root and the module path(s) with the `__init__.py[i]` below to it while /// Returns the source root and the module path(s) with the `__init__.py[i]` below to it while
/// checking the project layout and names. /// checking the project layout and names.
/// ///
@ -213,6 +270,7 @@ fn find_roots(
relative_module_root: &Path, relative_module_root: &Path,
module_name: Option<&ModuleName>, module_name: Option<&ModuleName>,
namespace: bool, namespace: bool,
show_warnings: bool,
) -> Result<(PathBuf, Vec<PathBuf>), Error> { ) -> Result<(PathBuf, Vec<PathBuf>), Error> {
let relative_module_root = uv_fs::normalize_path(relative_module_root); let relative_module_root = uv_fs::normalize_path(relative_module_root);
// Check that even if a path contains `..`, we only include files below the module root. // Check that even if a path contains `..`, we only include files below the module root.
@ -231,8 +289,8 @@ fn find_roots(
ModuleName::Name(name) => { ModuleName::Name(name) => {
vec![name.split('.').collect::<PathBuf>()] vec![name.split('.').collect::<PathBuf>()]
} }
ModuleName::Names(names) => names ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings)
.iter() .into_iter()
.map(|name| name.split('.').collect::<PathBuf>()) .map(|name| name.split('.').collect::<PathBuf>())
.collect(), .collect(),
} }
@ -250,9 +308,9 @@ fn find_roots(
let modules_relative = if let Some(module_name) = module_name { let modules_relative = if let Some(module_name) = module_name {
match module_name { match module_name {
ModuleName::Name(name) => vec![module_path_from_module_name(&src_root, name)?], ModuleName::Name(name) => vec![module_path_from_module_name(&src_root, name)?],
ModuleName::Names(names) => names ModuleName::Names(names) => prune_redundant_modules_warn(names, show_warnings)
.iter() .into_iter()
.map(|name| module_path_from_module_name(&src_root, name)) .map(|name| module_path_from_module_name(&src_root, &name))
.collect::<Result<_, _>>()?, .collect::<Result<_, _>>()?,
} }
} else { } else {
@ -420,19 +478,20 @@ mod tests {
fn build(source_root: &Path, dist: &Path) -> Result<BuildResults, Error> { fn build(source_root: &Path, dist: &Path) -> Result<BuildResults, Error> {
// Build a direct wheel, capture all its properties to compare it with the indirect wheel // Build a direct wheel, capture all its properties to compare it with the indirect wheel
// latest and remove it since it has the same filename as the indirect wheel. // latest and remove it since it has the same filename as the indirect wheel.
let (_name, direct_wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION)?; let (_name, direct_wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?;
let direct_wheel_filename = build_wheel(source_root, dist, None, MOCK_UV_VERSION)?; let direct_wheel_filename = build_wheel(source_root, dist, None, MOCK_UV_VERSION, false)?;
let direct_wheel_path = dist.join(direct_wheel_filename.to_string()); let direct_wheel_path = dist.join(direct_wheel_filename.to_string());
let direct_wheel_contents = wheel_contents(&direct_wheel_path); let direct_wheel_contents = wheel_contents(&direct_wheel_path);
let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?); let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?);
fs_err::remove_file(&direct_wheel_path)?; fs_err::remove_file(&direct_wheel_path)?;
// Build a source distribution. // Build a source distribution.
let (_name, source_dist_list_files) = list_source_dist(source_root, MOCK_UV_VERSION)?; let (_name, source_dist_list_files) =
list_source_dist(source_root, MOCK_UV_VERSION, false)?;
// TODO(konsti): This should run in the unpacked source dist tempdir, but we need to // TODO(konsti): This should run in the unpacked source dist tempdir, but we need to
// normalize the path. // normalize the path.
let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION)?; let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?;
let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION)?; let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION, false)?;
let source_dist_path = dist.join(source_dist_filename.to_string()); let source_dist_path = dist.join(source_dist_filename.to_string());
let source_dist_contents = sdist_contents(&source_dist_path); let source_dist_contents = sdist_contents(&source_dist_path);
@ -446,7 +505,13 @@ mod tests {
source_dist_filename.name.as_dist_info_name(), source_dist_filename.name.as_dist_info_name(),
source_dist_filename.version source_dist_filename.version
)); ));
let wheel_filename = build_wheel(&sdist_top_level_directory, dist, None, MOCK_UV_VERSION)?; let wheel_filename = build_wheel(
&sdist_top_level_directory,
dist,
None,
MOCK_UV_VERSION,
false,
)?;
let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string())); let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string()));
// Check that direct and indirect wheels are identical. // Check that direct and indirect wheels are identical.
@ -756,7 +821,7 @@ mod tests {
// Build a wheel from a source distribution // Build a wheel from a source distribution
let output_dir = TempDir::new().unwrap(); let output_dir = TempDir::new().unwrap();
build_source_dist(src.path(), output_dir.path(), "0.5.15").unwrap(); build_source_dist(src.path(), output_dir.path(), "0.5.15", false).unwrap();
let sdist_tree = TempDir::new().unwrap(); let sdist_tree = TempDir::new().unwrap();
let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz"); let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz");
let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap()); let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap());
@ -767,6 +832,7 @@ mod tests {
output_dir.path(), output_dir.path(),
None, None,
"0.5.15", "0.5.15",
false,
) )
.unwrap(); .unwrap();
let wheel = output_dir let wheel = output_dir
@ -831,6 +897,7 @@ mod tests {
output_dir.path(), output_dir.path(),
Some(&metadata_dir.path().join(&dist_info_dir)), Some(&metadata_dir.path().join(&dist_info_dir)),
"0.5.15", "0.5.15",
false,
) )
.unwrap(); .unwrap();
let wheel = output_dir let wheel = output_dir
@ -1414,4 +1481,114 @@ mod tests {
simple_namespace_part-1.0.0.dist-info/WHEEL simple_namespace_part-1.0.0.dist-info/WHEEL
"); ");
} }
/// `prune_redundant_modules` should remove modules which are already
/// included (either directly or via their parent)
#[test]
fn test_prune_redundant_modules() {
fn check(input: &[&str], expect: &[&str]) {
let input = input.iter().map(|s| (*s).to_string()).collect();
let expect: Vec<_> = expect.iter().map(|s| (*s).to_string()).collect();
assert_eq!(prune_redundant_modules(input), expect);
}
// Basic cases
check(&[], &[]);
check(&["foo"], &["foo"]);
check(&["foo", "bar"], &["bar", "foo"]);
// Deshadowing
check(&["foo", "foo.bar"], &["foo"]);
check(&["foo.bar", "foo"], &["foo"]);
check(
&["foo.bar.a", "foo.bar.b", "foo.bar", "foo", "foo.bar.a.c"],
&["foo"],
);
check(
&["bar.one", "bar.two", "baz", "bar", "baz.one"],
&["bar", "baz"],
);
// Potential false positives
check(&["foo", "foobar"], &["foo", "foobar"]);
check(
&["foo", "foobar", "foo.bar", "foobar.baz"],
&["foo", "foobar"],
);
check(&["foo.bar", "foo.baz"], &["foo.bar", "foo.baz"]);
check(&["foo", "foo", "foo.bar", "foo.bar"], &["foo"]);
// Everything
check(
&[
"foo.inner",
"foo.inner.deeper",
"foo",
"bar",
"bar.sub",
"bar.sub.deep",
"foobar",
"baz.baz.bar",
"baz.baz",
"qux",
],
&["bar", "baz.baz", "foo", "foobar", "qux"],
);
}
/// A package with duplicate module names.
#[test]
fn duplicate_module_names() {
let src = TempDir::new().unwrap();
let pyproject_toml = indoc! {r#"
[project]
name = "duplicate"
version = "1.0.0"
[tool.uv.build-backend]
module-name = ["foo", "foo", "bar.baz", "bar.baz.submodule"]
[build-system]
requires = ["uv_build>=0.5.15,<0.6.0"]
build-backend = "uv_build"
"#
};
fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap();
fs_err::create_dir_all(src.path().join("src").join("foo")).unwrap();
File::create(src.path().join("src").join("foo").join("__init__.py")).unwrap();
fs_err::create_dir_all(src.path().join("src").join("bar").join("baz")).unwrap();
File::create(
src.path()
.join("src")
.join("bar")
.join("baz")
.join("__init__.py"),
)
.unwrap();
let dist = TempDir::new().unwrap();
let build = build(src.path(), dist.path()).unwrap();
assert_snapshot!(build.source_dist_contents.join("\n"), @r"
duplicate-1.0.0/
duplicate-1.0.0/PKG-INFO
duplicate-1.0.0/pyproject.toml
duplicate-1.0.0/src
duplicate-1.0.0/src/bar
duplicate-1.0.0/src/bar/baz
duplicate-1.0.0/src/bar/baz/__init__.py
duplicate-1.0.0/src/foo
duplicate-1.0.0/src/foo/__init__.py
");
assert_snapshot!(build.wheel_contents.join("\n"), @r"
bar/
bar/baz/
bar/baz/__init__.py
duplicate-1.0.0.dist-info/
duplicate-1.0.0.dist-info/METADATA
duplicate-1.0.0.dist-info/RECORD
duplicate-1.0.0.dist-info/WHEEL
foo/
foo/__init__.py
");
}
} }

View File

@ -24,6 +24,7 @@ pub fn build_source_dist(
source_tree: &Path, source_tree: &Path,
source_dist_directory: &Path, source_dist_directory: &Path,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<SourceDistFilename, Error> { ) -> Result<SourceDistFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?; let pyproject_toml = PyProjectToml::parse(&contents)?;
@ -34,7 +35,7 @@ pub fn build_source_dist(
}; };
let source_dist_path = source_dist_directory.join(filename.to_string()); let source_dist_path = source_dist_directory.join(filename.to_string());
let writer = TarGzWriter::new(&source_dist_path)?; let writer = TarGzWriter::new(&source_dist_path)?;
write_source_dist(source_tree, writer, uv_version)?; write_source_dist(source_tree, writer, uv_version, show_warnings)?;
Ok(filename) Ok(filename)
} }
@ -42,6 +43,7 @@ pub fn build_source_dist(
pub fn list_source_dist( pub fn list_source_dist(
source_tree: &Path, source_tree: &Path,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<(SourceDistFilename, FileList), Error> { ) -> Result<(SourceDistFilename, FileList), Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?; let pyproject_toml = PyProjectToml::parse(&contents)?;
@ -52,7 +54,7 @@ pub fn list_source_dist(
}; };
let mut files = FileList::new(); let mut files = FileList::new();
let writer = ListWriter::new(&mut files); let writer = ListWriter::new(&mut files);
write_source_dist(source_tree, writer, uv_version)?; write_source_dist(source_tree, writer, uv_version, show_warnings)?;
Ok((filename, files)) Ok((filename, files))
} }
@ -61,6 +63,7 @@ fn source_dist_matcher(
source_tree: &Path, source_tree: &Path,
pyproject_toml: &PyProjectToml, pyproject_toml: &PyProjectToml,
settings: BuildBackendSettings, settings: BuildBackendSettings,
show_warnings: bool,
) -> Result<(GlobDirFilter, GlobSet), Error> { ) -> Result<(GlobDirFilter, GlobSet), Error> {
// File and directories to include in the source directory // File and directories to include in the source directory
let mut include_globs = Vec::new(); let mut include_globs = Vec::new();
@ -75,6 +78,7 @@ fn source_dist_matcher(
&settings.module_root, &settings.module_root,
settings.module_name.as_ref(), settings.module_name.as_ref(),
settings.namespace, settings.namespace,
show_warnings,
)?; )?;
for module_relative in modules_relative { for module_relative in modules_relative {
// The wheel must not include any files included by the source distribution (at least until we // The wheel must not include any files included by the source distribution (at least until we
@ -182,6 +186,7 @@ fn write_source_dist(
source_tree: &Path, source_tree: &Path,
mut writer: impl DirectoryWriter, mut writer: impl DirectoryWriter,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<SourceDistFilename, Error> { ) -> Result<SourceDistFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?; let pyproject_toml = PyProjectToml::parse(&contents)?;
@ -218,7 +223,7 @@ fn write_source_dist(
)?; )?;
let (include_matcher, exclude_matcher) = let (include_matcher, exclude_matcher) =
source_dist_matcher(source_tree, &pyproject_toml, settings)?; source_dist_matcher(source_tree, &pyproject_toml, settings, show_warnings)?;
let mut files_visited = 0; let mut files_visited = 0;
for entry in WalkDir::new(source_tree) for entry in WalkDir::new(source_tree)

View File

@ -29,6 +29,7 @@ pub fn build_wheel(
wheel_dir: &Path, wheel_dir: &Path,
metadata_directory: Option<&Path>, metadata_directory: Option<&Path>,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<WheelFilename, Error> { ) -> Result<WheelFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?; let pyproject_toml = PyProjectToml::parse(&contents)?;
@ -58,6 +59,7 @@ pub fn build_wheel(
&filename, &filename,
uv_version, uv_version,
wheel_writer, wheel_writer,
show_warnings,
)?; )?;
Ok(filename) Ok(filename)
@ -67,6 +69,7 @@ pub fn build_wheel(
pub fn list_wheel( pub fn list_wheel(
source_tree: &Path, source_tree: &Path,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<(WheelFilename, FileList), Error> { ) -> Result<(WheelFilename, FileList), Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?; let pyproject_toml = PyProjectToml::parse(&contents)?;
@ -87,7 +90,14 @@ pub fn list_wheel(
let mut files = FileList::new(); let mut files = FileList::new();
let writer = ListWriter::new(&mut files); let writer = ListWriter::new(&mut files);
write_wheel(source_tree, &pyproject_toml, &filename, uv_version, writer)?; write_wheel(
source_tree,
&pyproject_toml,
&filename,
uv_version,
writer,
show_warnings,
)?;
Ok((filename, files)) Ok((filename, files))
} }
@ -97,6 +107,7 @@ fn write_wheel(
filename: &WheelFilename, filename: &WheelFilename,
uv_version: &str, uv_version: &str,
mut wheel_writer: impl DirectoryWriter, mut wheel_writer: impl DirectoryWriter,
show_warnings: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
let settings = pyproject_toml let settings = pyproject_toml
.settings() .settings()
@ -132,6 +143,7 @@ fn write_wheel(
&settings.module_root, &settings.module_root,
settings.module_name.as_ref(), settings.module_name.as_ref(),
settings.namespace, settings.namespace,
show_warnings,
)?; )?;
let mut files_visited = 0; let mut files_visited = 0;
@ -259,6 +271,7 @@ pub fn build_editable(
wheel_dir: &Path, wheel_dir: &Path,
metadata_directory: Option<&Path>, metadata_directory: Option<&Path>,
uv_version: &str, uv_version: &str,
show_warnings: bool,
) -> Result<WheelFilename, Error> { ) -> Result<WheelFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?; let pyproject_toml = PyProjectToml::parse(&contents)?;
@ -295,6 +308,7 @@ pub fn build_editable(
&settings.module_root, &settings.module_root,
settings.module_name.as_ref(), settings.module_name.as_ref(),
settings.namespace, settings.namespace,
show_warnings,
)?; )?;
wheel_writer.write_bytes( wheel_writer.write_bytes(

View File

@ -44,6 +44,7 @@ fn main() -> Result<()> {
&env::current_dir()?, &env::current_dir()?,
&sdist_directory, &sdist_directory,
uv_version::version(), uv_version::version(),
false,
)?; )?;
// Tell the build frontend about the name of the artifact we built // Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
@ -56,6 +57,7 @@ fn main() -> Result<()> {
&wheel_directory, &wheel_directory,
metadata_directory.as_deref(), metadata_directory.as_deref(),
uv_version::version(), uv_version::version(),
false,
)?; )?;
// Tell the build frontend about the name of the artifact we built // Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
@ -68,6 +70,7 @@ fn main() -> Result<()> {
&wheel_directory, &wheel_directory,
metadata_directory.as_deref(), metadata_directory.as_deref(),
uv_version::version(), uv_version::version(),
false,
)?; )?;
// Tell the build frontend about the name of the artifact we built // Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;

View File

@ -504,6 +504,7 @@ impl BuildContext for BuildDispatch<'_> {
source: &'data Path, source: &'data Path,
subdirectory: Option<&'data Path>, subdirectory: Option<&'data Path>,
output_dir: &'data Path, output_dir: &'data Path,
sources: SourceStrategy,
build_kind: BuildKind, build_kind: BuildKind,
version_id: Option<&'data str>, version_id: Option<&'data str>,
) -> Result<Option<DistFilename>, BuildDispatchError> { ) -> Result<Option<DistFilename>, BuildDispatchError> {
@ -532,6 +533,7 @@ impl BuildContext for BuildDispatch<'_> {
&output_dir, &output_dir,
None, None,
uv_version::version(), uv_version::version(),
sources == SourceStrategy::Enabled,
)?; )?;
DistFilename::WheelFilename(wheel) DistFilename::WheelFilename(wheel)
} }
@ -540,6 +542,7 @@ impl BuildContext for BuildDispatch<'_> {
&source_tree, &source_tree,
&output_dir, &output_dir,
uv_version::version(), uv_version::version(),
sources == SourceStrategy::Enabled,
)?; )?;
DistFilename::SourceDistFilename(source_dist) DistFilename::SourceDistFilename(source_dist)
} }
@ -549,6 +552,7 @@ impl BuildContext for BuildDispatch<'_> {
&output_dir, &output_dir,
None, None,
uv_version::version(), uv_version::version(),
sources == SourceStrategy::Enabled,
)?; )?;
DistFilename::WheelFilename(wheel) DistFilename::WheelFilename(wheel)
} }

View File

@ -2435,6 +2435,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
source_root, source_root,
subdirectory, subdirectory,
temp_dir.path(), temp_dir.path(),
source_strategy,
if source.is_editable() { if source.is_editable() {
BuildKind::Editable BuildKind::Editable
} else { } else {

View File

@ -158,6 +158,7 @@ pub trait BuildContext {
source: &'a Path, source: &'a Path,
subdirectory: Option<&'a Path>, subdirectory: Option<&'a Path>,
output_dir: &'a Path, output_dir: &'a Path,
sources: SourceStrategy,
build_kind: BuildKind, build_kind: BuildKind,
version_id: Option<&'a str>, version_id: Option<&'a str>,
) -> impl Future<Output = Result<Option<DistFilename>, impl IsBuildBackendError>> + 'a; ) -> impl Future<Output = Result<Option<DistFilename>, impl IsBuildBackendError>> + 'a;

View File

@ -10,6 +10,7 @@ pub(crate) fn build_sdist(sdist_directory: &Path) -> Result<ExitStatus> {
&env::current_dir()?, &env::current_dir()?,
sdist_directory, sdist_directory,
uv_version::version(), uv_version::version(),
false,
)?; )?;
// Tell the build frontend about the name of the artifact we built // Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
@ -26,6 +27,7 @@ pub(crate) fn build_wheel(
wheel_directory, wheel_directory,
metadata_directory, metadata_directory,
uv_version::version(), uv_version::version(),
false,
)?; )?;
// Tell the build frontend about the name of the artifact we built // Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;
@ -42,6 +44,7 @@ pub(crate) fn build_editable(
wheel_directory, wheel_directory,
metadata_directory, metadata_directory,
uv_version::version(), uv_version::version(),
false,
)?; )?;
// Tell the build frontend about the name of the artifact we built // Tell the build frontend about the name of the artifact we built
writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?;

View File

@ -908,7 +908,11 @@ async fn build_sdist(
BuildAction::List => { BuildAction::List => {
let source_tree_ = source_tree.to_path_buf(); let source_tree_ = source_tree.to_path_buf();
let (filename, file_list) = tokio::task::spawn_blocking(move || { let (filename, file_list) = tokio::task::spawn_blocking(move || {
uv_build_backend::list_source_dist(&source_tree_, uv_version::version()) uv_build_backend::list_source_dist(
&source_tree_,
uv_version::version(),
sources == SourceStrategy::Enabled,
)
}) })
.await??; .await??;
let raw_filename = filename.to_string(); let raw_filename = filename.to_string();
@ -937,6 +941,7 @@ async fn build_sdist(
&source_tree, &source_tree,
&output_dir_, &output_dir_,
uv_version::version(), uv_version::version(),
sources == SourceStrategy::Enabled,
) )
}) })
.await?? .await??
@ -1013,7 +1018,11 @@ async fn build_wheel(
BuildAction::List => { BuildAction::List => {
let source_tree_ = source_tree.to_path_buf(); let source_tree_ = source_tree.to_path_buf();
let (filename, file_list) = tokio::task::spawn_blocking(move || { let (filename, file_list) = tokio::task::spawn_blocking(move || {
uv_build_backend::list_wheel(&source_tree_, uv_version::version()) uv_build_backend::list_wheel(
&source_tree_,
uv_version::version(),
sources == SourceStrategy::Enabled,
)
}) })
.await??; .await??;
let raw_filename = filename.to_string(); let raw_filename = filename.to_string();
@ -1043,6 +1052,7 @@ async fn build_wheel(
&output_dir_, &output_dir_,
None, None,
uv_version::version(), uv_version::version(),
sources == SourceStrategy::Enabled,
) )
}) })
.await??; .await??;

View File

@ -1117,3 +1117,71 @@ fn venv_in_source_tree() {
Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: src/foo/.venv Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: src/foo/.venv
"); ");
} }
/// Show a warning when the build backend is passed redundant module names
#[test]
fn warn_on_redundant_module_names() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-name = ["foo", "foo.bar", "foo", "foo.bar.baz", "foobar", "bar", "foobar.baz", "baz.bar"]
"#})?;
let foo_module = context.temp_dir.child("src/foo");
foo_module.create_dir_all()?;
foo_module.child("__init__.py").touch()?;
let foobar_module = context.temp_dir.child("src/foobar");
foobar_module.create_dir_all()?;
foobar_module.child("__init__.py").touch()?;
let bazbar_module = context.temp_dir.child("src/baz/bar");
bazbar_module.create_dir_all()?;
bazbar_module.child("__init__.py").touch()?;
let bar_module = context.temp_dir.child("src/bar");
bar_module.create_dir_all()?;
bar_module.child("__init__.py").touch()?;
// Warnings should be printed when invoking `uv build`
uv_snapshot!(context.filters(), context.build(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Building source distribution (uv build backend)...
warning: Ignoring redundant module names in `tool.uv.build-backend.module-name`: `foo.bar`, `foo`, `foo.bar.baz`, `foobar.baz`
Building wheel from source distribution (uv build backend)...
Successfully built dist/project-0.1.0.tar.gz
Successfully built dist/project-0.1.0-py3-none-any.whl
");
// But warnings shouldn't be printed in cases when the user might not
// control the thing being built. Sources being enabled is a workable proxy
// for this.
uv_snapshot!(context.filters(), context.build().arg("--no-sources"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Building source distribution (uv build backend)...
Building wheel from source distribution (uv build backend)...
Successfully built dist/project-0.1.0.tar.gz
Successfully built dist/project-0.1.0-py3-none-any.whl
");
Ok(())
}