From 39fe2d9eac21fd2c31cc8925691f00d7d681effc Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 8 Sep 2025 15:53:16 +0200 Subject: [PATCH] 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 --- crates/uv-build-backend/src/lib.rs | 9 +- crates/uv-build-backend/src/settings.rs | 14 +-- crates/uv-build-backend/src/source_dist.rs | 14 ++- crates/uv-build-backend/src/wheel.rs | 17 +++- crates/uv/tests/it/build.rs | 12 +-- crates/uv/tests/it/build_backend.rs | 105 ++++++++++++++++++++- 6 files changed, 151 insertions(+), 20 deletions(-) diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index aa90a88c5..683feb486 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -66,6 +66,9 @@ pub enum Error { /// Either an absolute path or a parent path through `..`. #[error("Module root must be inside the project: `{}`", _0.user_display())] 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}`")] InconsistentSteps(&'static str), #[error("Failed to write to {}", _0.user_display())] @@ -209,8 +212,10 @@ fn find_roots( namespace: bool, ) -> Result<(PathBuf, Vec), Error> { let relative_module_root = uv_fs::normalize_path(relative_module_root); - let src_root = source_tree.join(&relative_module_root); - if !src_root.starts_with(source_tree) { + // Check that even if a path contains `..`, we only include files below the module root. + 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())); } let src_root = source_tree.join(&relative_module_root); diff --git a/crates/uv-build-backend/src/settings.rs b/crates/uv-build-backend/src/settings.rs index 33e6d8c45..da90b509f 100644 --- a/crates/uv-build-backend/src/settings.rs +++ b/crates/uv-build-backend/src/settings.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use uv_macros::OptionsMetadata; /// Settings for the uv build backend (`uv_build`). @@ -204,16 +204,16 @@ pub enum ModuleName { #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct WheelDataIncludes { - purelib: Option, - platlib: Option, - headers: Option, - scripts: Option, - data: Option, + purelib: Option, + platlib: Option, + headers: Option, + scripts: Option, + data: Option, } impl WheelDataIncludes { /// Yield all data directories name and corresponding paths. - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { [ ("purelib", self.purelib.as_deref()), ("platlib", self.platlib.as_deref()), diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index 3b6d11ba4..81eaea5e1 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -9,7 +9,7 @@ use fs_err::File; use globset::{Glob, GlobSet}; use std::io; use std::io::{BufReader, Cursor}; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use tar::{EntryType, Header}; use tracing::{debug, trace}; use uv_distribution_filename::{SourceDistExtension, SourceDistFilename}; @@ -123,12 +123,22 @@ fn source_dist_matcher( // Include the data files for (name, directory) in settings.data.iter() { - let directory = uv_fs::normalize_path(Path::new(directory)); + let directory = uv_fs::normalize_path(directory); trace!( "Including data ({}) at: `{}`", name, 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 glob = PortableGlobParser::Uv .parse(&format!("{}/**", globset::escape(&directory))) diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index 7009a6468..762424a26 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -5,7 +5,7 @@ use itertools::Itertools; use rustc_hash::FxHashSet; use sha2::{Digest, Sha256}; use std::io::{BufReader, Read, Write}; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use std::{io, mem}; use tracing::{debug, trace}; use walkdir::WalkDir; @@ -207,7 +207,20 @@ fn write_wheel( // Add the data files 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!( "{}-{}.data/{}/", pyproject_toml.name().as_dist_info_name(), diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 138a218bb..3759c5b5b 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -1442,7 +1442,7 @@ fn build_fast_path() -> Result<()> { uv_snapshot!(context.build() .arg(&built_by_uv) .arg("--out-dir") - .arg(context.temp_dir.join("output1")), @r###" + .arg(context.temp_dir.join("output1")), @r" success: true exit_code: 0 ----- stdout ----- @@ -1452,7 +1452,7 @@ fn build_fast_path() -> Result<()> { 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-py3-none-any.whl - "###); + "); context .temp_dir .child("output1") @@ -1487,7 +1487,7 @@ fn build_fast_path() -> Result<()> { .arg(&built_by_uv) .arg("--out-dir") .arg(context.temp_dir.join("output3")) - .arg("--wheel"), @r###" + .arg("--wheel"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1495,7 +1495,7 @@ fn build_fast_path() -> Result<()> { ----- stderr ----- Building wheel (uv build backend)... Successfully built output3/built_by_uv-0.1.0-py3-none-any.whl - "###); + "); context .temp_dir .child("output3") @@ -1507,7 +1507,7 @@ fn build_fast_path() -> Result<()> { .arg("--out-dir") .arg(context.temp_dir.join("output4")) .arg("--sdist") - .arg("--wheel"), @r###" + .arg("--wheel"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1517,7 +1517,7 @@ fn build_fast_path() -> Result<()> { 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-py3-none-any.whl - "###); + "); context .temp_dir .child("output4") diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index ae3a7a740..350f3a522 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -1,7 +1,7 @@ use crate::common::{TestContext, uv_snapshot, venv_bin_path}; use anyhow::Result; 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 fs_err::File; use indoc::{formatdoc, indoc}; @@ -884,3 +884,106 @@ fn invalid_build_backend_settings_are_ignored() -> Result<()> { 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(()) +}