diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 402623c2d..a2ac6c593 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -85,8 +85,9 @@ pub enum Error { module_name: Identifier, paths: Vec, }, - #[error("Absolute module root is not allowed: `{}`", _0.display())] - AbsoluteModuleRoot(PathBuf), + /// Either an absolute path or a parent path through `..`. + #[error("Module root must be inside the project: `{}`", _0.user_display())] + InvalidModuleRoot(PathBuf), #[error("Inconsistent metadata between prepare and build step: `{0}`")] InconsistentSteps(&'static str), #[error("Failed to write to {}", _0.user_display())] @@ -203,12 +204,10 @@ fn find_roots( relative_module_root: &Path, module_name: Option<&Identifier>, ) -> Result<(PathBuf, PathBuf), Error> { - if relative_module_root.is_absolute() { - return Err(Error::AbsoluteModuleRoot( - relative_module_root.to_path_buf(), - )); + let src_root = source_tree.join(uv_fs::normalize_path(relative_module_root)); + if !src_root.starts_with(source_tree) { + 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 { module_name.clone() @@ -288,6 +287,7 @@ mod tests { /// source tree -> wheel /// and a build with /// source tree -> source dist -> wheel. + #[derive(Debug, PartialEq, Eq)] struct BuildResults { source_dist_list_files: FileList, source_dist_filename: SourceDistFilename, @@ -694,4 +694,73 @@ mod tests { 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); + } } diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index da26e02ad..2b2b69589 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -79,12 +79,10 @@ fn source_dist_matcher( // 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). - let import_path = &settings - .module_root - .join(module_name.as_ref()) + let import_path = uv_fs::normalize_path(&settings.module_root.join(module_name.as_ref())) .portable_display() .to_string(); - includes.push(format!("{}/**", globset::escape(import_path))); + includes.push(format!("{}/**", globset::escape(&import_path))); for include in includes { let glob = PortableGlobParser::Uv .parse(&include) @@ -92,7 +90,7 @@ fn source_dist_matcher( field: "tool.uv.build-backend.source-include".to_string(), source: err, })?; - include_globs.push(glob.clone()); + include_globs.push(glob); } // Include the Readme @@ -101,11 +99,11 @@ fn source_dist_matcher( .as_ref() .and_then(|readme| readme.path()) { + let readme = uv_fs::normalize_path(readme); trace!("Including readme at: `{}`", readme.user_display()); - include_globs.push( - Glob::new(&globset::escape(&readme.portable_display().to_string())) - .expect("escaped globset is parseable"), - ); + let readme = readme.portable_display().to_string(); + let glob = Glob::new(&globset::escape(&readme)).expect("escaped globset is parseable"); + include_globs.push(glob); } // Include the license files @@ -122,13 +120,19 @@ fn source_dist_matcher( // Include the data files 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 - .parse(&format!("{}/**", globset::escape(directory))) + .parse(&format!("{}/**", globset::escape(&directory))) .map_err(|err| Error::PortableGlob { field: format!("tool.uv.build-backend.data.{name}"), source: err, })?; - trace!("Including data ({name}) at: `{directory}`"); include_globs.push(glob); } diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index d3126f88a..b8e14967f 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -268,10 +268,10 @@ pub fn build_editable( let mut wheel_writer = ZipDirectoryWriter::new_wheel(File::create(&wheel_path)?); debug!("Adding pth file to {}", wheel_path.user_display()); - if settings.module_root.is_absolute() { - return Err(Error::AbsoluteModuleRoot(settings.module_root.clone())); + let src_root = source_tree.join(&settings.module_root); + 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 { module_name