mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-10-31 12:06:13 +00:00 
			
		
		
		
	Error early for parent path in build backend (#15733)
Paths referencing above the directory of the `pyproject.toml`, such as `module-root = ".."`, are not supported by the build backend. The check that should catch was not working properly, so the source distribution built successfully and only the wheel build failed. We now error early. The same fix is applied to data includes. Fix #15702
This commit is contained in:
		
							parent
							
								
									5f8c7181b9
								
							
						
					
					
						commit
						39fe2d9eac
					
				
					 6 changed files with 151 additions and 20 deletions
				
			
		|  | @ -66,6 +66,9 @@ pub enum Error { | ||||||
|     /// Either an absolute path or a parent path through `..`.
 |     /// Either an absolute path or a parent path through `..`.
 | ||||||
|     #[error("Module root must be inside the project: `{}`", _0.user_display())] |     #[error("Module root must be inside the project: `{}`", _0.user_display())] | ||||||
|     InvalidModuleRoot(PathBuf), |     InvalidModuleRoot(PathBuf), | ||||||
|  |     /// 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("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())] | ||||||
|  | @ -209,8 +212,10 @@ fn find_roots( | ||||||
|     namespace: bool, |     namespace: 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); | ||||||
|     let src_root = source_tree.join(&relative_module_root); |     // Check that even if a path contains `..`, we only include files below the module root.
 | ||||||
|     if !src_root.starts_with(source_tree) { |     if !uv_fs::normalize_path(&source_tree.join(&relative_module_root)) | ||||||
|  |         .starts_with(uv_fs::normalize_path(source_tree)) | ||||||
|  |     { | ||||||
|         return Err(Error::InvalidModuleRoot(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 src_root = source_tree.join(&relative_module_root); | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use std::path::PathBuf; | use std::path::{Path, PathBuf}; | ||||||
| use uv_macros::OptionsMetadata; | use uv_macros::OptionsMetadata; | ||||||
| 
 | 
 | ||||||
| /// Settings for the uv build backend (`uv_build`).
 | /// Settings for the uv build backend (`uv_build`).
 | ||||||
|  | @ -204,16 +204,16 @@ pub enum ModuleName { | ||||||
| #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] | #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] | ||||||
| #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] | ||||||
| pub struct WheelDataIncludes { | pub struct WheelDataIncludes { | ||||||
|     purelib: Option<String>, |     purelib: Option<PathBuf>, | ||||||
|     platlib: Option<String>, |     platlib: Option<PathBuf>, | ||||||
|     headers: Option<String>, |     headers: Option<PathBuf>, | ||||||
|     scripts: Option<String>, |     scripts: Option<PathBuf>, | ||||||
|     data: Option<String>, |     data: Option<PathBuf>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl WheelDataIncludes { | impl WheelDataIncludes { | ||||||
|     /// Yield all data directories name and corresponding paths.
 |     /// Yield all data directories name and corresponding paths.
 | ||||||
|     pub fn iter(&self) -> impl Iterator<Item = (&'static str, &str)> { |     pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Path)> { | ||||||
|         [ |         [ | ||||||
|             ("purelib", self.purelib.as_deref()), |             ("purelib", self.purelib.as_deref()), | ||||||
|             ("platlib", self.platlib.as_deref()), |             ("platlib", self.platlib.as_deref()), | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ use fs_err::File; | ||||||
| use globset::{Glob, GlobSet}; | use globset::{Glob, GlobSet}; | ||||||
| use std::io; | use std::io; | ||||||
| use std::io::{BufReader, Cursor}; | use std::io::{BufReader, Cursor}; | ||||||
| use std::path::{Path, PathBuf}; | use std::path::{Component, Path, PathBuf}; | ||||||
| use tar::{EntryType, Header}; | use tar::{EntryType, Header}; | ||||||
| use tracing::{debug, trace}; | use tracing::{debug, trace}; | ||||||
| use uv_distribution_filename::{SourceDistExtension, SourceDistFilename}; | use uv_distribution_filename::{SourceDistExtension, SourceDistFilename}; | ||||||
|  | @ -123,12 +123,22 @@ 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)); |         let directory = uv_fs::normalize_path(directory); | ||||||
|         trace!( |         trace!( | ||||||
|             "Including data ({}) at: `{}`", |             "Including data ({}) at: `{}`", | ||||||
|             name, |             name, | ||||||
|             directory.user_display() |             directory.user_display() | ||||||
|         ); |         ); | ||||||
|  |         if directory | ||||||
|  |             .components() | ||||||
|  |             .next() | ||||||
|  |             .is_some_and(|component| !matches!(component, Component::CurDir | Component::Normal(_))) | ||||||
|  |         { | ||||||
|  |             return Err(Error::InvalidDataRoot { | ||||||
|  |                 name: name.to_string(), | ||||||
|  |                 path: directory.to_path_buf(), | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|         let directory = directory.portable_display().to_string(); |         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))) | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ use itertools::Itertools; | ||||||
| use rustc_hash::FxHashSet; | use rustc_hash::FxHashSet; | ||||||
| use sha2::{Digest, Sha256}; | use sha2::{Digest, Sha256}; | ||||||
| use std::io::{BufReader, Read, Write}; | use std::io::{BufReader, Read, Write}; | ||||||
| use std::path::{Path, PathBuf}; | use std::path::{Component, Path, PathBuf}; | ||||||
| use std::{io, mem}; | use std::{io, mem}; | ||||||
| use tracing::{debug, trace}; | use tracing::{debug, trace}; | ||||||
| use walkdir::WalkDir; | use walkdir::WalkDir; | ||||||
|  | @ -207,7 +207,20 @@ fn write_wheel( | ||||||
| 
 | 
 | ||||||
|     // Add the data files
 |     // Add the data files
 | ||||||
|     for (name, directory) in settings.data.iter() { |     for (name, directory) in settings.data.iter() { | ||||||
|         debug!("Adding {name} data files from: `{directory}`"); |         debug!( | ||||||
|  |             "Adding {name} data files from: `{}`", | ||||||
|  |             directory.user_display() | ||||||
|  |         ); | ||||||
|  |         if directory | ||||||
|  |             .components() | ||||||
|  |             .next() | ||||||
|  |             .is_some_and(|component| !matches!(component, Component::CurDir | Component::Normal(_))) | ||||||
|  |         { | ||||||
|  |             return Err(Error::InvalidDataRoot { | ||||||
|  |                 name: name.to_string(), | ||||||
|  |                 path: directory.to_path_buf(), | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|         let data_dir = format!( |         let data_dir = format!( | ||||||
|             "{}-{}.data/{}/", |             "{}-{}.data/{}/", | ||||||
|             pyproject_toml.name().as_dist_info_name(), |             pyproject_toml.name().as_dist_info_name(), | ||||||
|  |  | ||||||
|  | @ -1442,7 +1442,7 @@ fn build_fast_path() -> Result<()> { | ||||||
|     uv_snapshot!(context.build() |     uv_snapshot!(context.build() | ||||||
|         .arg(&built_by_uv) |         .arg(&built_by_uv) | ||||||
|         .arg("--out-dir") |         .arg("--out-dir") | ||||||
|         .arg(context.temp_dir.join("output1")), @r###" |         .arg(context.temp_dir.join("output1")), @r" | ||||||
|     success: true |     success: true | ||||||
|     exit_code: 0 |     exit_code: 0 | ||||||
|     ----- stdout ----- |     ----- stdout ----- | ||||||
|  | @ -1452,7 +1452,7 @@ fn build_fast_path() -> Result<()> { | ||||||
|     Building wheel from source distribution (uv build backend)... |     Building wheel from source distribution (uv build backend)... | ||||||
|     Successfully built output1/built_by_uv-0.1.0.tar.gz |     Successfully built output1/built_by_uv-0.1.0.tar.gz | ||||||
|     Successfully built output1/built_by_uv-0.1.0-py3-none-any.whl |     Successfully built output1/built_by_uv-0.1.0-py3-none-any.whl | ||||||
|     "###);
 |     ");
 | ||||||
|     context |     context | ||||||
|         .temp_dir |         .temp_dir | ||||||
|         .child("output1") |         .child("output1") | ||||||
|  | @ -1487,7 +1487,7 @@ fn build_fast_path() -> Result<()> { | ||||||
|         .arg(&built_by_uv) |         .arg(&built_by_uv) | ||||||
|         .arg("--out-dir") |         .arg("--out-dir") | ||||||
|         .arg(context.temp_dir.join("output3")) |         .arg(context.temp_dir.join("output3")) | ||||||
|         .arg("--wheel"), @r###" |         .arg("--wheel"), @r" | ||||||
|     success: true |     success: true | ||||||
|     exit_code: 0 |     exit_code: 0 | ||||||
|     ----- stdout ----- |     ----- stdout ----- | ||||||
|  | @ -1495,7 +1495,7 @@ fn build_fast_path() -> Result<()> { | ||||||
|     ----- stderr ----- |     ----- stderr ----- | ||||||
|     Building wheel (uv build backend)... |     Building wheel (uv build backend)... | ||||||
|     Successfully built output3/built_by_uv-0.1.0-py3-none-any.whl |     Successfully built output3/built_by_uv-0.1.0-py3-none-any.whl | ||||||
|     "###);
 |     ");
 | ||||||
|     context |     context | ||||||
|         .temp_dir |         .temp_dir | ||||||
|         .child("output3") |         .child("output3") | ||||||
|  | @ -1507,7 +1507,7 @@ fn build_fast_path() -> Result<()> { | ||||||
|         .arg("--out-dir") |         .arg("--out-dir") | ||||||
|         .arg(context.temp_dir.join("output4")) |         .arg(context.temp_dir.join("output4")) | ||||||
|         .arg("--sdist") |         .arg("--sdist") | ||||||
|         .arg("--wheel"), @r###" |         .arg("--wheel"), @r" | ||||||
|     success: true |     success: true | ||||||
|     exit_code: 0 |     exit_code: 0 | ||||||
|     ----- stdout ----- |     ----- stdout ----- | ||||||
|  | @ -1517,7 +1517,7 @@ fn build_fast_path() -> Result<()> { | ||||||
|     Building wheel (uv build backend)... |     Building wheel (uv build backend)... | ||||||
|     Successfully built output4/built_by_uv-0.1.0.tar.gz |     Successfully built output4/built_by_uv-0.1.0.tar.gz | ||||||
|     Successfully built output4/built_by_uv-0.1.0-py3-none-any.whl |     Successfully built output4/built_by_uv-0.1.0-py3-none-any.whl | ||||||
|     "###);
 |     ");
 | ||||||
|     context |     context | ||||||
|         .temp_dir |         .temp_dir | ||||||
|         .child("output4") |         .child("output4") | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| use crate::common::{TestContext, uv_snapshot, venv_bin_path}; | use crate::common::{TestContext, uv_snapshot, venv_bin_path}; | ||||||
| use anyhow::Result; | use anyhow::Result; | ||||||
| use assert_cmd::assert::OutputAssertExt; | use assert_cmd::assert::OutputAssertExt; | ||||||
| use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir}; | use assert_fs::fixture::{FileTouch, FileWriteStr, PathChild, PathCreateDir}; | ||||||
| use flate2::bufread::GzDecoder; | use flate2::bufread::GzDecoder; | ||||||
| use fs_err::File; | use fs_err::File; | ||||||
| use indoc::{formatdoc, indoc}; | use indoc::{formatdoc, indoc}; | ||||||
|  | @ -884,3 +884,106 @@ fn invalid_build_backend_settings_are_ignored() -> Result<()> { | ||||||
| 
 | 
 | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /// Error when there is a relative module root outside the project root, such as
 | ||||||
|  | /// `tool.uv.build-backend.module-root = ".."`.
 | ||||||
|  | #[test] | ||||||
|  | fn error_on_relative_module_root_outside_project_root() -> 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" | ||||||
|  | 
 | ||||||
|  |         [tool.uv.build-backend] | ||||||
|  |         module-root = ".." | ||||||
|  | 
 | ||||||
|  |         [build-system] | ||||||
|  |         requires = ["uv_build>=0.7,<10000"] | ||||||
|  |         build-backend = "uv_build" | ||||||
|  |     "#})?;
 | ||||||
|  | 
 | ||||||
|  |     context.temp_dir.child("__init__.py").touch()?; | ||||||
|  | 
 | ||||||
|  |     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]/` | ||||||
|  |       ╰─▶ Module root must be inside the project: `..` | ||||||
|  |     ");
 | ||||||
|  | 
 | ||||||
|  |     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]/` | ||||||
|  |       ╰─▶ Module root must be inside the project: `..` | ||||||
|  |     ");
 | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Error when there is a relative data directory outside the project root, such as
 | ||||||
|  | /// `tool.uv.build-backend.data.headers = "../headers"`.
 | ||||||
|  | #[test] | ||||||
|  | fn error_on_relative_data_dir_outside_project_root() -> Result<()> { | ||||||
|  |     let context = TestContext::new("3.12"); | ||||||
|  | 
 | ||||||
|  |     let project = context.temp_dir.child("project"); | ||||||
|  |     project.create_dir_all()?; | ||||||
|  | 
 | ||||||
|  |     let pyproject_toml = project.child("pyproject.toml"); | ||||||
|  |     pyproject_toml.write_str(indoc! {r#" | ||||||
|  |         [project] | ||||||
|  |         name = "project" | ||||||
|  |         version = "0.1.0" | ||||||
|  |         requires-python = ">=3.12" | ||||||
|  | 
 | ||||||
|  |         [tool.uv.build-backend.data] | ||||||
|  |         headers = "../header" | ||||||
|  | 
 | ||||||
|  |         [build-system] | ||||||
|  |         requires = ["uv_build>=0.7,<10000"] | ||||||
|  |         build-backend = "uv_build" | ||||||
|  |     "#})?;
 | ||||||
|  | 
 | ||||||
|  |     let project_module = project.child("src/project"); | ||||||
|  |     project_module.create_dir_all()?; | ||||||
|  |     project_module.child("__init__.py").touch()?; | ||||||
|  | 
 | ||||||
|  |     context.temp_dir.child("headers").create_dir_all()?; | ||||||
|  | 
 | ||||||
|  |     uv_snapshot!(context.filters(), context.build().arg("project"), @r" | ||||||
|  |     success: false | ||||||
|  |     exit_code: 2 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     Building source distribution (uv build backend)... | ||||||
|  |       × Failed to build `[TEMP_DIR]/project` | ||||||
|  |       ╰─▶ The path for the data directory headers must be inside the project: `../header` | ||||||
|  |     ");
 | ||||||
|  | 
 | ||||||
|  |     uv_snapshot!(context.filters(), context.build().arg("project").arg("--wheel"), @r" | ||||||
|  |     success: false | ||||||
|  |     exit_code: 2 | ||||||
|  |     ----- stdout ----- | ||||||
|  | 
 | ||||||
|  |     ----- stderr ----- | ||||||
|  |     Building wheel (uv build backend)... | ||||||
|  |       × Failed to build `[TEMP_DIR]/project` | ||||||
|  |       ╰─▶ The path for the data directory headers must be inside the project: `../header` | ||||||
|  |     ");
 | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 konsti
						konsti