Build backend: Normalize glob paths (#13465)

Unlike OS APIs, glob inclusion checks don't work when there are relative
path elements such as `./`. We normalize the path before using it for
the glob.

Fixes #13407
This commit is contained in:
konsti 2025-05-15 17:19:02 +02:00 committed by GitHub
parent 7a83f51de2
commit 18a1f0d9db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 94 additions and 21 deletions

View file

@ -85,8 +85,9 @@ pub enum Error {
module_name: Identifier, module_name: Identifier,
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
}, },
#[error("Absolute module root is not allowed: `{}`", _0.display())] /// Either an absolute path or a parent path through `..`.
AbsoluteModuleRoot(PathBuf), #[error("Module root must be inside the project: `{}`", _0.user_display())]
InvalidModuleRoot(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())]
@ -203,12 +204,10 @@ fn find_roots(
relative_module_root: &Path, relative_module_root: &Path,
module_name: Option<&Identifier>, module_name: Option<&Identifier>,
) -> Result<(PathBuf, PathBuf), Error> { ) -> Result<(PathBuf, PathBuf), Error> {
if relative_module_root.is_absolute() { let src_root = source_tree.join(uv_fs::normalize_path(relative_module_root));
return Err(Error::AbsoluteModuleRoot( if !src_root.starts_with(source_tree) {
relative_module_root.to_path_buf(), return Err(Error::InvalidModuleRoot(relative_module_root.to_path_buf()));
));
} }
let src_root = source_tree.join(relative_module_root);
let module_name = if let Some(module_name) = module_name { let module_name = if let Some(module_name) = module_name {
module_name.clone() module_name.clone()
@ -288,6 +287,7 @@ mod tests {
/// source tree -> wheel /// source tree -> wheel
/// and a build with /// and a build with
/// source tree -> source dist -> wheel. /// source tree -> source dist -> wheel.
#[derive(Debug, PartialEq, Eq)]
struct BuildResults { struct BuildResults {
source_dist_list_files: FileList, source_dist_list_files: FileList,
source_dist_filename: SourceDistFilename, source_dist_filename: SourceDistFilename,
@ -694,4 +694,73 @@ mod tests {
Version: 1.0.0 Version: 1.0.0
"###); "###);
} }
/// Check that non-normalized paths for `module-root` work with the glob inclusions.
#[test]
fn test_glob_path_normalization() {
let src = TempDir::new().unwrap();
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "two-step-build"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-root = "./"
"#
},
)
.unwrap();
fs_err::create_dir_all(src.path().join("two_step_build")).unwrap();
File::create(src.path().join("two_step_build").join("__init__.py")).unwrap();
let dist = TempDir::new().unwrap();
let build1 = build(src.path(), dist.path());
assert_snapshot!(build1.source_dist_contents.join("\n"), @r"
two_step_build-1.0.0/
two_step_build-1.0.0/PKG-INFO
two_step_build-1.0.0/pyproject.toml
two_step_build-1.0.0/two_step_build
two_step_build-1.0.0/two_step_build/__init__.py
");
assert_snapshot!(build1.wheel_contents.join("\n"), @r"
two_step_build-1.0.0.dist-info/
two_step_build-1.0.0.dist-info/METADATA
two_step_build-1.0.0.dist-info/RECORD
two_step_build-1.0.0.dist-info/WHEEL
two_step_build/
two_step_build/__init__.py
");
// A path with a parent reference.
fs_err::write(
src.path().join("pyproject.toml"),
indoc! {r#"
[project]
name = "two-step-build"
version = "1.0.0"
[build-system]
requires = ["uv_build>=0.5.15,<0.6"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-root = "two_step_build/.././"
"#
},
)
.unwrap();
let dist = TempDir::new().unwrap();
let build2 = build(src.path(), dist.path());
assert_eq!(build1, build2);
}
} }

View file

@ -79,12 +79,10 @@ fn source_dist_matcher(
// 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
// have files generated in the source dist -> wheel build step). // have files generated in the source dist -> wheel build step).
let import_path = &settings let import_path = uv_fs::normalize_path(&settings.module_root.join(module_name.as_ref()))
.module_root
.join(module_name.as_ref())
.portable_display() .portable_display()
.to_string(); .to_string();
includes.push(format!("{}/**", globset::escape(import_path))); includes.push(format!("{}/**", globset::escape(&import_path)));
for include in includes { for include in includes {
let glob = PortableGlobParser::Uv let glob = PortableGlobParser::Uv
.parse(&include) .parse(&include)
@ -92,7 +90,7 @@ fn source_dist_matcher(
field: "tool.uv.build-backend.source-include".to_string(), field: "tool.uv.build-backend.source-include".to_string(),
source: err, source: err,
})?; })?;
include_globs.push(glob.clone()); include_globs.push(glob);
} }
// Include the Readme // Include the Readme
@ -101,11 +99,11 @@ fn source_dist_matcher(
.as_ref() .as_ref()
.and_then(|readme| readme.path()) .and_then(|readme| readme.path())
{ {
let readme = uv_fs::normalize_path(readme);
trace!("Including readme at: `{}`", readme.user_display()); trace!("Including readme at: `{}`", readme.user_display());
include_globs.push( let readme = readme.portable_display().to_string();
Glob::new(&globset::escape(&readme.portable_display().to_string())) let glob = Glob::new(&globset::escape(&readme)).expect("escaped globset is parseable");
.expect("escaped globset is parseable"), include_globs.push(glob);
);
} }
// Include the license files // Include the license files
@ -122,13 +120,19 @@ fn source_dist_matcher(
// Include the data files // Include the data files
for (name, directory) in settings.data.iter() { for (name, directory) in settings.data.iter() {
let directory = uv_fs::normalize_path(Path::new(directory));
trace!(
"Including data ({}) at: `{}`",
name,
directory.user_display()
);
let directory = directory.portable_display().to_string();
let glob = PortableGlobParser::Uv let glob = PortableGlobParser::Uv
.parse(&format!("{}/**", globset::escape(directory))) .parse(&format!("{}/**", globset::escape(&directory)))
.map_err(|err| Error::PortableGlob { .map_err(|err| Error::PortableGlob {
field: format!("tool.uv.build-backend.data.{name}"), field: format!("tool.uv.build-backend.data.{name}"),
source: err, source: err,
})?; })?;
trace!("Including data ({name}) at: `{directory}`");
include_globs.push(glob); include_globs.push(glob);
} }

View file

@ -268,10 +268,10 @@ pub fn build_editable(
let mut wheel_writer = ZipDirectoryWriter::new_wheel(File::create(&wheel_path)?); let mut wheel_writer = ZipDirectoryWriter::new_wheel(File::create(&wheel_path)?);
debug!("Adding pth file to {}", wheel_path.user_display()); debug!("Adding pth file to {}", wheel_path.user_display());
if settings.module_root.is_absolute() { let src_root = source_tree.join(&settings.module_root);
return Err(Error::AbsoluteModuleRoot(settings.module_root.clone())); if !src_root.starts_with(source_tree) {
return Err(Error::InvalidModuleRoot(settings.module_root.clone()));
} }
let src_root = source_tree.join(settings.module_root);
let module_name = if let Some(module_name) = settings.module_name { let module_name = if let Some(module_name) = settings.module_name {
module_name module_name