diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 5af696e216563..911c1d0025a06 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -1,6 +1,8 @@ mod metadata; -use crate::metadata::{BuildBackendSettings, PyProjectToml, ValidationError, DEFAULT_EXCLUDES}; +pub use metadata::PyProjectToml; + +use crate::metadata::{BuildBackendSettings, ValidationError, DEFAULT_EXCLUDES}; use flate2::write::GzEncoder; use flate2::Compression; use fs_err::File; @@ -304,7 +306,9 @@ pub fn build_wheel( ) -> Result { let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&contents)?; - pyproject_toml.check_build_system(uv_version); + for warning in pyproject_toml.check_build_system(uv_version) { + warn_user_once!("{warning}"); + } let settings = pyproject_toml .settings() .cloned() @@ -465,7 +469,9 @@ pub fn build_editable( ) -> Result { let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&contents)?; - pyproject_toml.check_build_system(uv_version); + for warning in pyproject_toml.check_build_system(uv_version) { + warn_user_once!("{warning}"); + } let settings = pyproject_toml .settings() .cloned() @@ -601,7 +607,9 @@ pub fn build_source_dist( ) -> Result { let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&contents)?; - pyproject_toml.check_build_system(uv_version); + for warning in pyproject_toml.check_build_system(uv_version) { + warn_user_once!("{warning}"); + } let settings = pyproject_toml .settings() .cloned() @@ -851,7 +859,9 @@ pub fn metadata( ) -> Result { let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&contents)?; - pyproject_toml.check_build_system(uv_version); + for warning in pyproject_toml.check_build_system(uv_version) { + warn_user_once!("{warning}"); + } let filename = WheelFilename { name: pyproject_toml.name().clone(), diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index d6fede06bed9a..54a9f62d383f5 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -12,7 +12,6 @@ use uv_normalize::{ExtraName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::{Requirement, VersionOrUrl}; use uv_pypi_types::{Metadata23, VerbatimParsedUrl}; -use uv_warnings::warn_user_once; use version_ranges::Ranges; use walkdir::WalkDir; @@ -58,7 +57,7 @@ pub enum ValidationError { expecting = "The project table needs to follow \ https://packaging.python.org/en/latest/guides/writing-pyproject-toml" )] -pub(crate) struct PyProjectToml { +pub struct PyProjectToml { /// Project metadata project: Project, /// uv-specific configuration @@ -92,7 +91,7 @@ impl PyProjectToml { self.tool.as_ref()?.uv.as_ref()?.build_backend.as_ref() } - /// Warn if the `[build-system]` table looks suspicious. + /// Returns user-facing warnings if the `[build-system]` table looks suspicious. /// /// Example of a valid table: /// @@ -101,16 +100,13 @@ impl PyProjectToml { /// requires = ["uv>=0.4.15,<5"] /// build-backend = "uv" /// ``` - /// - /// Returns whether all checks passed. - pub(crate) fn check_build_system(&self, uv_version: &str) -> bool { - let mut passed = true; + pub fn check_build_system(&self, uv_version: &str) -> Vec { + let mut warnings = Vec::new(); if self.build_system.build_backend.as_deref() != Some("uv") { - warn_user_once!( + warnings.push(format!( r#"The value for `build_system.build-backend` should be `"uv"`, not `"{}"`"#, self.build_system.build_backend.clone().unwrap_or_default() - ); - passed = false; + )); } let uv_version = @@ -126,12 +122,12 @@ impl PyProjectToml { }; let [uv_requirement] = &self.build_system.requires.as_slice() else { - warn_user_once!("{}", expected()); - return false; + warnings.push(expected()); + return warnings; }; if uv_requirement.name.as_str() != "uv" { - warn_user_once!("{}", expected()); - return false; + warnings.push(expected()); + return warnings; } let bounded = match &uv_requirement.version_or_url { None => false, @@ -147,11 +143,10 @@ impl PyProjectToml { // the existing immutable source distributions on pypi. if !specifier.contains(&uv_version) { // This is allowed to happen when testing prereleases, but we should still warn. - warn_user_once!( + warnings.push(format!( r#"`build_system.requires = ["{uv_requirement}"]` does not contain the current uv version {uv_version}"#, - ); - passed = false; + )); } Ranges::from(specifier.clone()) .bounding_range() @@ -161,16 +156,15 @@ impl PyProjectToml { }; if !bounded { - warn_user_once!( - r#"`build_system.requires = ["{uv_requirement}"]` is missing an - upper bound on the uv version such as `<{next_breaking}`. - Without bounding the uv version, the source distribution will break - when a future, breaking version of uv is released."#, - ); - passed = false; + warnings.push(format!( + "`build_system.requires = [\"{uv_requirement}\"]` is missing an \ + upper bound on the uv version such as `<{next_breaking}`. \ + Without bounding the uv version, the source distribution will break \ + when a future, breaking version of uv is released.", + )); } - passed + warnings } /// Validate and convert a `pyproject.toml` to core metadata. @@ -995,7 +989,10 @@ mod tests { fn build_system_valid() { let contents = extend_project(""); let pyproject_toml = PyProjectToml::parse(&contents).unwrap(); - assert!(pyproject_toml.check_build_system("1.0.0+test")); + assert_snapshot!( + pyproject_toml.check_build_system("1.0.0+test").join("\n"), + @"" + ); } #[test] @@ -1010,7 +1007,10 @@ mod tests { build-backend = "uv" "#}; let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - assert!(!pyproject_toml.check_build_system("1.0.0+test")); + assert_snapshot!( + pyproject_toml.check_build_system("0.4.15+test").join("\n"), + @r###"`build_system.requires = ["uv"]` is missing an upper bound on the uv version such as `<0.5`. Without bounding the uv version, the source distribution will break when a future, breaking version of uv is released."### + ); } #[test] @@ -1025,7 +1025,10 @@ mod tests { build-backend = "uv" "#}; let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - assert!(!pyproject_toml.check_build_system("1.0.0+test")); + assert_snapshot!( + pyproject_toml.check_build_system("0.4.15+test").join("\n"), + @"Expected a single uv requirement in `build-system.requires`, found ``" + ); } #[test] @@ -1040,7 +1043,10 @@ mod tests { build-backend = "uv" "#}; let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - assert!(!pyproject_toml.check_build_system("1.0.0+test")); + assert_snapshot!( + pyproject_toml.check_build_system("0.4.15+test").join("\n"), + @"Expected a single uv requirement in `build-system.requires`, found ``" + ); } #[test] @@ -1055,7 +1061,10 @@ mod tests { build-backend = "setuptools" "#}; let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - assert!(!pyproject_toml.check_build_system("1.0.0+test")); + assert_snapshot!( + pyproject_toml.check_build_system("0.4.15+test").join("\n"), + @r###"The value for `build_system.build-backend` should be `"uv"`, not `"setuptools"`"### + ); } #[test] diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 29c2522c905b2..ef2eafb683a0e 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2178,6 +2178,14 @@ pub struct BuildArgs { #[arg(long, overrides_with("build_logs"), hide = true)] pub no_build_logs: bool, + /// Always build through PEP 517, don't use the fast path for the uv build backend. + /// + /// By default, uv won't create a PEP 517 build environment for packages using the uv build + /// backend, but use a fast path that calls into the build backend directly. This option forces + /// always using PEP 517. + #[arg(long)] + pub no_fast_path: bool, + /// Constrain build dependencies using the given requirements files when building /// distributions. /// diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 1b701a3b989fa..9151e1fb77a44 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -7,11 +7,13 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use owo_colors::OwoColorize; +use tracing::{debug, trace}; use uv_distribution_filename::SourceDistExtension; -use uv_distribution_types::{DependencyMetadata, Index, IndexLocations}; +use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, SourceDist}; use uv_install_wheel::linker::LinkMode; use uv_auth::store_credentials; +use uv_build_backend::PyProjectToml; use uv_cache::{Cache, CacheBucket}; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ @@ -50,6 +52,7 @@ pub(crate) async fn build_frontend( sdist: bool, wheel: bool, build_logs: bool, + no_fast_path: bool, build_constraints: Vec, hash_checking: Option, python: Option, @@ -74,6 +77,7 @@ pub(crate) async fn build_frontend( sdist, wheel, build_logs, + no_fast_path, &build_constraints, hash_checking, python.as_deref(), @@ -116,6 +120,7 @@ async fn build_impl( sdist: bool, wheel: bool, build_logs: bool, + no_fast_path: bool, build_constraints: &[RequirementsSource], hash_checking: Option, python_request: Option<&str>, @@ -266,6 +271,7 @@ async fn build_impl( &client_builder, hash_checking, build_logs, + no_fast_path, build_constraints, no_build_isolation, no_build_isolation_package, @@ -362,6 +368,7 @@ async fn build_package( client_builder: &BaseClientBuilder<'_>, hash_checking: Option, build_logs: bool, + no_fast_path: bool, build_constraints: &[RequirementsSource], no_build_isolation: bool, no_build_isolation_package: &[PackageName], @@ -524,48 +531,14 @@ async fn build_package( concurrency, ); - // Create the output directory. - fs_err::tokio::create_dir_all(&output_dir).await?; - - // Add a .gitignore. - match fs_err::OpenOptions::new() - .write(true) - .create_new(true) - .open(output_dir.join(".gitignore")) - { - Ok(mut file) => file.write_all(b"*")?, - Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (), - Err(err) => return Err(err.into()), - } + prepare_output_directory(&output_dir).await?; // Determine the build plan. - let plan = match &source.source { - Source::File(_) => { - // We're building from a file, which must be a source distribution. - match (sdist, wheel) { - (false, true) => BuildPlan::WheelFromSdist, - (false, false) => { - return Err(anyhow::anyhow!( - "Pass `--wheel` explicitly to build a wheel from a source distribution" - )); - } - (true, _) => { - return Err(anyhow::anyhow!( - "Building an `--sdist` from a source distribution is not supported" - )); - } - } - } - Source::Directory(_) => { - // We're building from a directory. - match (sdist, wheel) { - (false, false) => BuildPlan::SdistToWheel, - (false, true) => BuildPlan::Wheel, - (true, false) => BuildPlan::Sdist, - (true, true) => BuildPlan::SdistAndWheel, - } - } - }; + let plan = BuildPlan::determine(&source, sdist, wheel)?; + + // Check if the build backend is matching uv version that allows calling in the uv build backend + // directly. + let fast_path = !no_fast_path && check_fast_path(source.path()); // Prepare some common arguments for the build. let dist = None; @@ -585,26 +558,21 @@ async fn build_package( let assets = match plan { BuildPlan::SdistToWheel => { - writeln!( - printer.stderr(), - "{}", - source.annotate("Building source distribution...").bold() - )?; - - // Build the sdist. - let builder = build_dispatch - .setup_build( - source.path(), - subdirectory, - source.path(), - version_id.map(ToString::to_string), - dist, - sources, - BuildKind::Sdist, - build_output, - ) - .await?; - let sdist = builder.build(&output_dir).await?; + let sdist = build_sdist( + source.path(), + &output_dir, + fast_path, + &source, + printer, + "Building source distribution", + &build_dispatch, + sources, + dist, + subdirectory, + version_id, + build_output, + ) + .await?; // Extract the source distribution into a temporary directory. let path = output_dir.join(&sdist); @@ -622,131 +590,105 @@ async fn build_package( Err(err) => return Err(err.into()), }; - writeln!( - printer.stderr(), - "{}", - source - .annotate("Building wheel from source distribution...") - .bold() - )?; - - // Build a wheel from the source distribution. - let builder = build_dispatch - .setup_build( - &extracted, - subdirectory, - source.path(), - version_id.map(ToString::to_string), - dist, - sources, - BuildKind::Wheel, - build_output, - ) - .await?; - let wheel = builder.build(&output_dir).await?; + let wheel = build_wheel( + &extracted, + &output_dir, + fast_path, + &source, + printer, + "Building wheel from source distribution", + &build_dispatch, + sources, + dist, + subdirectory, + version_id, + build_output, + ) + .await?; BuiltDistributions::Both(output_dir.join(sdist), output_dir.join(wheel)) } BuildPlan::Sdist => { - writeln!( - printer.stderr(), - "{}", - source.annotate("Building source distribution...").bold() - )?; - - let builder = build_dispatch - .setup_build( - source.path(), - subdirectory, - source.path(), - version_id.map(ToString::to_string), - dist, - sources, - BuildKind::Sdist, - build_output, - ) - .await?; - let sdist = builder.build(&output_dir).await?; + let sdist = build_sdist( + source.path(), + &output_dir, + fast_path, + &source, + printer, + "Building source distribution", + &build_dispatch, + sources, + dist, + subdirectory, + version_id, + build_output, + ) + .await?; BuiltDistributions::Sdist(output_dir.join(sdist)) } BuildPlan::Wheel => { - writeln!( - printer.stderr(), - "{}", - source.annotate("Building wheel...").bold() - )?; - - let builder = build_dispatch - .setup_build( - source.path(), - subdirectory, - source.path(), - version_id.map(ToString::to_string), - dist, - sources, - BuildKind::Wheel, - build_output, - ) - .await?; - let wheel = builder.build(&output_dir).await?; + let wheel = build_wheel( + source.path(), + &output_dir, + fast_path, + &source, + printer, + "Building wheel", + &build_dispatch, + sources, + dist, + subdirectory, + version_id, + build_output, + ) + .await?; BuiltDistributions::Wheel(output_dir.join(wheel)) } BuildPlan::SdistAndWheel => { - writeln!( - printer.stderr(), - "{}", - source.annotate("Building source distribution...").bold() - )?; - let builder = build_dispatch - .setup_build( - source.path(), - subdirectory, - source.path(), - version_id.map(ToString::to_string), - dist, - sources, - BuildKind::Sdist, - build_output, - ) - .await?; - let sdist = builder.build(&output_dir).await?; - - writeln!( - printer.stderr(), - "{}", - source.annotate("Building wheel...").bold() - )?; - let builder = build_dispatch - .setup_build( - source.path(), - subdirectory, - source.path(), - version_id.map(ToString::to_string), - dist, - sources, - BuildKind::Wheel, - build_output, - ) - .await?; - let wheel = builder.build(&output_dir).await?; + let sdist = build_sdist( + source.path(), + &output_dir, + fast_path, + &source, + printer, + "Building source distribution", + &build_dispatch, + sources, + dist, + subdirectory, + version_id, + build_output, + ) + .await?; + + let wheel = build_wheel( + source.path(), + &output_dir, + fast_path, + &source, + printer, + "Building wheel", + &build_dispatch, + sources, + dist, + subdirectory, + version_id, + build_output, + ) + .await?; BuiltDistributions::Both(output_dir.join(&sdist), output_dir.join(&wheel)) } BuildPlan::WheelFromSdist => { - writeln!( - printer.stderr(), - "{}", - source - .annotate("Building wheel from source distribution...") - .bold() - )?; - // Extract the source distribution into a temporary directory. let reader = fs_err::tokio::File::open(source.path()).await?; let ext = SourceDistExtension::from_path(source.path()).map_err(|err| { - anyhow::anyhow!("`{}` is not a valid build source. Expected to receive a source directory, or a source distribution ending in one of: {err}.", source.path().user_display()) + anyhow::anyhow!( + "`{}` is not a valid build source. Expected to receive a source directory, or a source distribution ending in one of: {err}.", + source.path().user_display() + ) })?; let temp_dir = tempfile::tempdir_in(&output_dir)?; uv_extract::stream::archive(reader, ext, temp_dir.path()).await?; @@ -758,20 +700,21 @@ async fn build_package( Err(err) => return Err(err.into()), }; - // Build a wheel from the source distribution. - let builder = build_dispatch - .setup_build( - &extracted, - subdirectory, - source.path(), - version_id.map(ToString::to_string), - dist, - sources, - BuildKind::Wheel, - build_output, - ) - .await?; - let wheel = builder.build(&output_dir).await?; + let wheel = build_wheel( + &extracted, + &output_dir, + fast_path, + &source, + printer, + "Building wheel from source distribution", + &build_dispatch, + sources, + dist, + subdirectory, + version_id, + build_output, + ) + .await?; BuiltDistributions::Wheel(output_dir.join(wheel)) } @@ -780,6 +723,138 @@ async fn build_package( Ok(assets) } +/// Build a source distribution, either through PEP 517 or through the fast path. +async fn build_sdist( + source_tree: &Path, + output_dir: &Path, + fast_path: bool, + source: &AnnotatedSource<'_>, + printer: Printer, + message: &str, + // Below is only used with PEP 517 builds + build_dispatch: &BuildDispatch<'_>, + sources: SourceStrategy, + dist: Option<&SourceDist>, + subdirectory: Option<&Path>, + version_id: Option<&str>, + build_output: BuildOutput, +) -> Result { + let sdist = if fast_path { + writeln!( + printer.stderr(), + "{}", + format!( + "{}{} (uv build backend)...", + source.message_prefix(), + message + ) + .bold() + )?; + let source_tree = source_tree.to_path_buf(); + let output_dir = output_dir.to_path_buf(); + tokio::task::spawn_blocking(move || { + uv_build_backend::build_source_dist(&source_tree, &output_dir, uv_version::version()) + }) + .await?? + .to_string() + } else { + writeln!( + printer.stderr(), + "{}", + format!("{}{}...", source.message_prefix(), message).bold() + )?; + let builder = build_dispatch + .setup_build( + source_tree, + subdirectory, + source.path(), + version_id.map(ToString::to_string), + dist, + sources, + BuildKind::Sdist, + build_output, + ) + .await?; + builder.build(output_dir).await? + }; + Ok(sdist) +} + +/// Build a wheel, either through PEP 517 or through the fast path. +async fn build_wheel( + source_tree: &Path, + output_dir: &Path, + fast_path: bool, + source: &AnnotatedSource<'_>, + printer: Printer, + message: &str, + // Below is only used with PEP 517 builds + build_dispatch: &BuildDispatch<'_>, + sources: SourceStrategy, + dist: Option<&SourceDist>, + subdirectory: Option<&Path>, + version_id: Option<&str>, + build_output: BuildOutput, +) -> Result { + let wheel = if fast_path { + writeln!( + printer.stderr(), + "{}", + format!( + "{}{} (uv build backend)...", + source.message_prefix(), + message + ) + .bold() + )?; + let source_tree = source_tree.to_path_buf(); + let output_dir = output_dir.to_path_buf(); + tokio::task::spawn_blocking(move || { + uv_build_backend::build_wheel(&source_tree, &output_dir, None, uv_version::version()) + }) + .await?? + .to_string() + } else { + writeln!( + printer.stderr(), + "{}", + format!("{}{}...", source.message_prefix(), message).bold() + )?; + let builder = build_dispatch + .setup_build( + source_tree, + subdirectory, + source.path(), + version_id.map(ToString::to_string), + dist, + sources, + BuildKind::Wheel, + build_output, + ) + .await?; + builder.build(output_dir).await? + }; + Ok(wheel) +} + +/// Create the output directory and add a `.gitignore`. +async fn prepare_output_directory(output_dir: &Path) -> Result<()> { + // Create the output directory. + fs_err::tokio::create_dir_all(&output_dir).await?; + + // Add a .gitignore. + match fs_err::OpenOptions::new() + .write(true) + .create_new(true) + .open(output_dir.join(".gitignore")) + { + Ok(mut file) => file.write_all(b"*")?, + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (), + Err(err) => return Err(err.into()), + } + Ok(()) +} + #[derive(Debug, Clone, PartialEq, Eq)] struct AnnotatedSource<'a> { /// The underlying [`Source`] to build. @@ -797,11 +872,11 @@ impl AnnotatedSource<'_> { self.source.directory() } - fn annotate<'a>(&self, s: &'a str) -> Cow<'a, str> { + fn message_prefix(&self) -> Cow<'_, str> { if let Some(package) = &self.package { - Cow::Owned(format!("[{}] {s}", package.cyan())) + Cow::Owned(format!("[{}] ", package.cyan())) } else { - Cow::Borrowed(s) + Cow::Borrowed("") } } } @@ -876,3 +951,65 @@ enum BuildPlan { /// Build a wheel from a source distribution. WheelFromSdist, } + +impl BuildPlan { + fn determine(source: &AnnotatedSource, sdist: bool, wheel: bool) -> Result { + Ok(match &source.source { + Source::File(_) => { + // We're building from a file, which must be a source distribution. + match (sdist, wheel) { + (false, true) => Self::WheelFromSdist, + (false, false) => { + return Err(anyhow::anyhow!( + "Pass `--wheel` explicitly to build a wheel from a source distribution" + )); + } + (true, _) => { + return Err(anyhow::anyhow!( + "Building an `--sdist` from a source distribution is not supported" + )); + } + } + } + Source::Directory(_) => { + // We're building from a directory. + match (sdist, wheel) { + (false, false) => Self::SdistToWheel, + (false, true) => Self::Wheel, + (true, false) => Self::Sdist, + (true, true) => Self::SdistAndWheel, + } + } + }) + } +} + +/// Check if the build backend is matching the currently running uv version. +fn check_fast_path(source_tree: &Path) -> bool { + let pyproject_toml: PyProjectToml = + match fs_err::read_to_string(source_tree.join("pyproject.toml")) + .map_err(anyhow::Error::from) + .and_then(|pyproject_toml| Ok(toml::from_str(&pyproject_toml)?)) + { + Ok(pyproject_toml) => pyproject_toml, + Err(err) => { + debug!("Not using uv build backend fast path, no pyproject.toml: {err}"); + return false; + } + }; + match pyproject_toml + .check_build_system(uv_version::version()) + .as_slice() + { + // No warnings -> match + [] => true, + // Any warning -> no match + [first, others @ ..] => { + debug!("Not using uv build backend fast path, pyproject.toml does not match: {first}"); + for other in others { + trace!("Further uv build backend fast path mismatch: {other}"); + } + false + } + } +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index a9dd0cdff2ef6..1796ec7746942 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -732,6 +732,7 @@ async fn run(mut cli: Cli) -> Result { args.sdist, args.wheel, args.build_logs, + args.no_fast_path, build_constraints, args.hash_checking, args.python, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index ff9fe6fc25c9d..9213ed4a819ed 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -2014,6 +2014,7 @@ pub(crate) struct BuildSettings { pub(crate) sdist: bool, pub(crate) wheel: bool, pub(crate) build_logs: bool, + pub(crate) no_fast_path: bool, pub(crate) build_constraints: Vec, pub(crate) hash_checking: Option, pub(crate) python: Option, @@ -2032,6 +2033,7 @@ impl BuildSettings { all_packages, sdist, wheel, + no_fast_path, build_constraints, require_hashes, no_require_hashes, @@ -2062,6 +2064,7 @@ impl BuildSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + no_fast_path, hash_checking: HashCheckingMode::from_args( flag(require_hashes, no_require_hashes), flag(verify_hashes, no_verify_hashes), diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index a133082a55bcd..aa9e604ab8733 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -5,6 +5,7 @@ use fs_err::File; use indoc::indoc; use insta::assert_snapshot; use predicates::prelude::predicate; +use std::env::current_dir; use zip::ZipArchive; #[test] @@ -817,7 +818,6 @@ fn wheel_from_sdist() -> Result<()> { ----- stdout ----- ----- stderr ----- - Building wheel from source distribution... × Failed to build `[TEMP_DIR]/project/dist/project-0.1.0-py3-none-any.whl` ╰─▶ `dist/project-0.1.0-py3-none-any.whl` is not a valid build source. Expected to receive a source directory, or a source distribution ending in one of: `.tar.gz`, `.zip`, `.tar.bz2`, `.tar.lz`, `.tar.lzma`, `.tar.xz`, `.tar.zst`, `.tar`, `.tbz`, `.tgz`, `.tlz`, or `.txz`. "###); @@ -2035,3 +2035,105 @@ fn build_non_package() -> Result<()> { Ok(()) } + +/// Test the uv fast path. Tests all four possible build plans: +/// * Defaults +/// * `--sdist` +/// * `--wheel` +/// * `--sdist --wheel` +#[test] +fn build_fast_path() -> Result<()> { + let context = TestContext::new("3.12"); + + let built_by_uv = current_dir()?.join("../../scripts/packages/built-by-uv"); + + uv_snapshot!(context.build() + .arg(&built_by_uv) + .arg("--out-dir") + .arg(context.temp_dir.join("output1")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + Building wheel from source distribution (uv build backend)... + Successfully built output1/built_by_uv-0.1.0.tar.gz and output1/built_by_uv-0.1.0-py3-none-any.whl + "###); + context + .temp_dir + .child("output1") + .child("built_by_uv-0.1.0.tar.gz") + .assert(predicate::path::is_file()); + context + .temp_dir + .child("output1") + .child("built_by_uv-0.1.0-py3-none-any.whl") + .assert(predicate::path::is_file()); + + uv_snapshot!(context.build() + .arg(&built_by_uv) + .arg("--out-dir") + .arg(context.temp_dir.join("output2")) + .arg("--sdist"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + Successfully built output2/built_by_uv-0.1.0.tar.gz + "###); + context + .temp_dir + .child("output2") + .child("built_by_uv-0.1.0.tar.gz") + .assert(predicate::path::is_file()); + + uv_snapshot!(context.build() + .arg(&built_by_uv) + .arg("--out-dir") + .arg(context.temp_dir.join("output3")) + .arg("--wheel"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building wheel (uv build backend)... + Successfully built output3/built_by_uv-0.1.0-py3-none-any.whl + "###); + context + .temp_dir + .child("output3") + .child("built_by_uv-0.1.0-py3-none-any.whl") + .assert(predicate::path::is_file()); + + uv_snapshot!(context.build() + .arg(&built_by_uv) + .arg("--out-dir") + .arg(context.temp_dir.join("output4")) + .arg("--sdist") + .arg("--wheel"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + Building wheel (uv build backend)... + Successfully built output4/built_by_uv-0.1.0.tar.gz and output4/built_by_uv-0.1.0-py3-none-any.whl + "###); + context + .temp_dir + .child("output4") + .child("built_by_uv-0.1.0.tar.gz") + .assert(predicate::path::is_file()); + context + .temp_dir + .child("output4") + .child("built_by_uv-0.1.0-py3-none-any.whl") + .assert(predicate::path::is_file()); + + Ok(()) +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d2735144d3d6d..268060e3eae51 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -7888,6 +7888,10 @@ uv build [OPTIONS] [SRC]

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

May also be set with the UV_NO_CONFIG environment variable.

+
--no-fast-path

Always build through PEP 517, don’t use the fast path for the uv build backend.

+ +

By default, uv won’t create a PEP 517 build environment for packages using the uv build backend, but use a fast path that calls into the build backend directly. This option forces always using PEP 517.

+
--no-index

Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via --find-links

--no-progress

Hide all progress outputs.