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:
konsti 2025-09-08 15:53:16 +02:00 committed by GitHub
parent 5f8c7181b9
commit 39fe2d9eac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 151 additions and 20 deletions

View file

@ -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<PathBuf>), 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);

View file

@ -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<String>,
platlib: Option<String>,
headers: Option<String>,
scripts: Option<String>,
data: Option<String>,
purelib: Option<PathBuf>,
platlib: Option<PathBuf>,
headers: Option<PathBuf>,
scripts: Option<PathBuf>,
data: Option<PathBuf>,
}
impl WheelDataIncludes {
/// 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()),
("platlib", self.platlib.as_deref()),

View file

@ -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)))

View file

@ -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(),

View file

@ -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")

View file

@ -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(())
}