Build backend: Add fast path (#9556)

Going through PEP 517 to build a package is slow, so when building a
package with the uv build backend, we can call into the uv build backend
directly. This is the basis for the `uv build --list`.

This does not enable the fast path for general source dependencies.

There is a possible difference in execution if the latest uv version is
newer than the one currently running: The PEP 517 path would use the
latest version, while the fast path uses the current version.

Please review commit-by-commit

### Benchmark

`built_with_uv`, using the fast path:
```
$ hyperfine "~/projects/uv/target/profiling/uv build"
Time (mean ± σ):       9.2 ms ±   1.1 ms    [User: 4.6 ms, System: 4.6 ms]
Range (min … max):     6.4 ms …  12.7 ms    290 runs
```

`hatcling_editable`, with hatchling being optimized for fast startup
times:
```
$ hyperfine "~/projects/uv/target/profiling/uv build"
Time (mean ± σ):     270.5 ms ±  18.4 ms    [User: 230.8 ms, System: 44.5 ms]
Range (min … max):   250.7 ms … 298.4 ms    10 runs
```
This commit is contained in:
konsti 2024-12-02 16:37:50 +01:00 committed by GitHub
parent 659e86efde
commit 5b27decbe7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 493 additions and 219 deletions

View file

@ -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<WheelFilename, Error> {
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<WheelFilename, Error> {
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<SourceDistFilename, Error> {
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<String, Error> {
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(),

View file

@ -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<String> {
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]

View file

@ -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.
///

View file

@ -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<RequirementsSource>,
hash_checking: Option<HashCheckingMode>,
python: Option<String>,
@ -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<HashCheckingMode>,
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<HashCheckingMode>,
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?;
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?;
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::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<String> {
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<String> {
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<Self> {
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
}
}
}

View file

@ -732,6 +732,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.sdist,
args.wheel,
args.build_logs,
args.no_fast_path,
build_constraints,
args.hash_checking,
args.python,

View file

@ -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<PathBuf>,
pub(crate) hash_checking: Option<HashCheckingMode>,
pub(crate) python: Option<String>,
@ -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),

View file

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

View file

@ -7888,6 +7888,10 @@ uv build [OPTIONS] [SRC]
<p>Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.</p>
<p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p>
</dd><dt><code>--no-fast-path</code></dt><dd><p>Always build through PEP 517, don&#8217;t use the fast path for the uv build backend.</p>
<p>By default, uv won&#8217;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.</p>
</dd><dt><code>--no-index</code></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>
</dd><dt><code>--no-progress</code></dt><dd><p>Hide all progress outputs.</p>