mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
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:
parent
659e86efde
commit
5b27decbe7
8 changed files with 493 additions and 219 deletions
|
@ -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(),
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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’t use the fast path for the uv build backend.</p>
|
||||
|
||||
<p>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.</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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue