From 999b3f06a414f78db4935eef7cfd13dc3d97143e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 15 Oct 2024 18:46:29 -0700 Subject: [PATCH] Respect relative paths in `uv build` sources (#8237) ## Summary Right now, `uv build` will fail if a package depends on a local source in `build-system.requires`. --- crates/uv-build-frontend/src/lib.rs | 9 +- crates/uv-dispatch/src/lib.rs | 2 + crates/uv-distribution/src/source/mod.rs | 2 + crates/uv-types/src/traits.rs | 1 + crates/uv/src/commands/build_frontend.rs | 9 +- crates/uv/tests/it/build.rs | 154 +++++++++++++++++++++++ 6 files changed, 174 insertions(+), 3 deletions(-) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index b4f4401e3..ca7eded39 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -245,6 +245,7 @@ impl SourceBuild { pub async fn setup( source: &Path, subdirectory: Option<&Path>, + install_path: &Path, fallback_package_name: Option<&PackageName>, fallback_package_version: Option<&Version>, interpreter: &Interpreter, @@ -273,6 +274,7 @@ impl SourceBuild { // Check if we have a PEP 517 build backend. let (pep517_backend, project) = Self::extract_pep517_backend( &source_tree, + install_path, fallback_package_name, locations, source_strategy, @@ -368,6 +370,7 @@ impl SourceBuild { create_pep517_build_environment( &runner, &source_tree, + install_path, &venv, &pep517_backend, build_context, @@ -436,6 +439,7 @@ impl SourceBuild { /// Extract the PEP 517 backend from the `pyproject.toml` or `setup.py` file. async fn extract_pep517_backend( source_tree: &Path, + install_path: &Path, package_name: Option<&PackageName>, locations: &IndexLocations, source_strategy: SourceStrategy, @@ -469,7 +473,7 @@ impl SourceBuild { }; let requires_dist = RequiresDist::from_project_maybe_workspace( requires_dist, - source_tree, + install_path, locations, source_strategy, LowerBound::Allow, @@ -803,6 +807,7 @@ fn escape_path_for_python(path: &Path) -> String { async fn create_pep517_build_environment( runner: &PythonRunner, source_tree: &Path, + install_path: &Path, venv: &PythonEnvironment, pep517_backend: &Pep517Backend, build_context: &impl BuildContext, @@ -921,7 +926,7 @@ async fn create_pep517_build_environment( }; let requires_dist = RequiresDist::from_project_maybe_workspace( requires_dist, - source_tree, + install_path, locations, source_strategy, LowerBound::Allow, diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 513f66f9a..6fc529a28 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -318,6 +318,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { &'data self, source: &'data Path, subdirectory: Option<&'data Path>, + install_path: &'data Path, version_id: Option, dist: Option<&'data SourceDist>, sources: SourceStrategy, @@ -352,6 +353,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { let builder = SourceBuild::setup( source, subdirectory, + install_path, dist_name, dist_version, self.interpreter, diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index a5492bded..a8e3d0113 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1712,6 +1712,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .setup_build( source_root, subdirectory, + source_root, Some(source.to_string()), source.as_dist(), source_strategy, @@ -1756,6 +1757,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .setup_build( source_root, subdirectory, + source_root, Some(source.to_string()), source.as_dist(), source_strategy, diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index 2ba16a3a4..72b84d95f 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -111,6 +111,7 @@ pub trait BuildContext { &'a self, source: &'a Path, subdirectory: Option<&'a Path>, + install_path: &'a Path, version_id: Option, dist: Option<&'a SourceDist>, sources: SourceStrategy, diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 5f0067fef..fa4e44bf4 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -535,9 +535,9 @@ async fn build_package( }; // Prepare some common arguments for the build. + let dist = None; let subdirectory = None; let version_id = source.path().file_name().and_then(|name| name.to_str()); - let dist = None; let build_output = match printer { Printer::Default | Printer::NoProgress | Printer::Verbose => { @@ -563,6 +563,7 @@ async fn build_package( .setup_build( source.path(), subdirectory, + source.path(), version_id.map(ToString::to_string), dist, sources, @@ -601,6 +602,7 @@ async fn build_package( .setup_build( &extracted, subdirectory, + source.path(), version_id.map(ToString::to_string), dist, sources, @@ -623,6 +625,7 @@ async fn build_package( .setup_build( source.path(), subdirectory, + source.path(), version_id.map(ToString::to_string), dist, sources, @@ -645,6 +648,7 @@ async fn build_package( .setup_build( source.path(), subdirectory, + source.path(), version_id.map(ToString::to_string), dist, sources, @@ -666,6 +670,7 @@ async fn build_package( .setup_build( source.path(), subdirectory, + source.path(), version_id.map(ToString::to_string), dist, sources, @@ -684,6 +689,7 @@ async fn build_package( .setup_build( source.path(), subdirectory, + source.path(), version_id.map(ToString::to_string), dist, sources, @@ -724,6 +730,7 @@ async fn build_package( .setup_build( &extracted, subdirectory, + source.path(), version_id.map(ToString::to_string), dist, sources, diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index bb8fe310f..1cde4b207 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -2,6 +2,7 @@ use crate::common::{uv_snapshot, TestContext}; use anyhow::Result; use assert_fs::prelude::*; use fs_err::File; +use indoc::indoc; use insta::assert_snapshot; use predicates::prelude::predicate; use zip::ZipArchive; @@ -1766,6 +1767,159 @@ fn build_no_build_logs() -> Result<()> { Ok(()) } +#[test] +fn tool_uv_sources() -> Result<()> { + let context = TestContext::new("3.12"); + let filters = context + .filters() + .into_iter() + .chain([ + (r"exit code: 1", "exit status: 1"), + (r"bdist\.[^/\\\s]+-[^/\\\s]+", "bdist.linux-x86_64"), + (r"\\\.", ""), + ]) + .collect::>(); + + let build = context.temp_dir.child("backend"); + build.child("pyproject.toml").write_str( + r#" + [project] + name = "backend" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions>=3.10"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + build + .child("src") + .child("backend") + .child("__init__.py") + .write_str(indoc! { r#" + def hello() -> str: + return "Hello, world!" + "#})?; + build.child("README.md").touch()?; + + let project = context.temp_dir.child("project"); + + project.child("pyproject.toml").write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>1"] + + [build-system] + requires = ["setuptools>=42", "backend==0.1.0"] + build-backend = "setuptools.build_meta" + + [tool.uv.sources] + backend = { path = "../backend" } + "#, + )?; + + project.child("setup.py").write_str(indoc! {r" + from setuptools import setup + + from backend import hello + + hello() + + setup() + ", + })?; + + uv_snapshot!(filters, context.build().current_dir(project.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building source distribution... + running egg_info + creating project.egg-info + writing project.egg-info/PKG-INFO + writing dependency_links to project.egg-info/dependency_links.txt + writing requirements to project.egg-info/requires.txt + writing top-level names to project.egg-info/top_level.txt + writing manifest file 'project.egg-info/SOURCES.txt' + reading manifest file 'project.egg-info/SOURCES.txt' + writing manifest file 'project.egg-info/SOURCES.txt' + running sdist + running egg_info + writing project.egg-info/PKG-INFO + writing dependency_links to project.egg-info/dependency_links.txt + writing requirements to project.egg-info/requires.txt + writing top-level names to project.egg-info/top_level.txt + reading manifest file 'project.egg-info/SOURCES.txt' + writing manifest file 'project.egg-info/SOURCES.txt' + warning: sdist: standard file not found: should have one of README, README.rst, README.txt, README.md + + running check + creating project-0.1.0 + creating project-0.1.0/project.egg-info + copying files to project-0.1.0... + copying pyproject.toml -> project-0.1.0 + copying setup.py -> project-0.1.0 + copying project.egg-info/PKG-INFO -> project-0.1.0/project.egg-info + copying project.egg-info/SOURCES.txt -> project-0.1.0/project.egg-info + copying project.egg-info/dependency_links.txt -> project-0.1.0/project.egg-info + copying project.egg-info/requires.txt -> project-0.1.0/project.egg-info + copying project.egg-info/top_level.txt -> project-0.1.0/project.egg-info + copying project.egg-info/SOURCES.txt -> project-0.1.0/project.egg-info + Writing project-0.1.0/setup.cfg + Creating tar archive + removing 'project-0.1.0' (and everything under it) + Building wheel from source distribution... + running egg_info + writing project.egg-info/PKG-INFO + writing dependency_links to project.egg-info/dependency_links.txt + writing requirements to project.egg-info/requires.txt + writing top-level names to project.egg-info/top_level.txt + reading manifest file 'project.egg-info/SOURCES.txt' + writing manifest file 'project.egg-info/SOURCES.txt' + running bdist_wheel + running build + installing to build/bdist.linux-x86_64/wheel + running install + running install_egg_info + running egg_info + writing project.egg-info/PKG-INFO + writing dependency_links to project.egg-info/dependency_links.txt + writing requirements to project.egg-info/requires.txt + writing top-level names to project.egg-info/top_level.txt + reading manifest file 'project.egg-info/SOURCES.txt' + writing manifest file 'project.egg-info/SOURCES.txt' + Copying project.egg-info to build/bdist.linux-x86_64/wheel/project-0.1.0-py3.12.egg-info + running install_scripts + creating build/bdist.linux-x86_64/wheel/project-0.1.0.dist-info/WHEEL + creating '[TEMP_DIR]/project/dist/[TMP]/wheel' to it + adding 'project-0.1.0.dist-info/METADATA' + adding 'project-0.1.0.dist-info/WHEEL' + adding 'project-0.1.0.dist-info/top_level.txt' + adding 'project-0.1.0.dist-info/RECORD' + removing build/bdist.linux-x86_64/wheel + Successfully built dist/project-0.1.0.tar.gz and dist/project-0.1.0-py3-none-any.whl + "###); + + project + .child("dist") + .child("project-0.1.0.tar.gz") + .assert(predicate::path::is_file()); + project + .child("dist") + .child("project-0.1.0-py3-none-any.whl") + .assert(predicate::path::is_file()); + + Ok(()) +} + /// Check that we have a working git boundary for builds from source dist to wheel in `dist/`. #[test] fn git_boundary_in_dist_build() -> Result<()> {