use std::borrow::Cow;
use std::fmt::Write as _;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{fmt, io};
use anyhow::{Context, Result};
use owo_colors::OwoColorize;
use thiserror::Error;
use tracing::{debug, instrument};
use uv_build_backend::check_direct_build;
use uv_cache::{Cache, CacheBucket};
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildIsolation, BuildKind, BuildOptions, BuildOutput, Concurrency, Constraints,
DependencyGroupsWithDefaults, HashCheckingMode, IndexStrategy, KeyringProviderType, NoSources,
};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_filename::{
DistFilename, SourceDistExtension, SourceDistFilename, WheelFilename,
};
use uv_distribution_types::{
ConfigSettings, DependencyMetadata, ExtraBuildVariables, Index, IndexLocations,
PackageConfigSettings, Requirement, SourceDist,
};
use uv_fs::{Simplified, relative_to};
use uv_install_wheel::LinkMode;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_preview::Preview;
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
PythonPreference, PythonRequest, PythonVersionFile, VersionFileDiscoveryOptions,
};
use uv_requirements::RequirementsSource;
use uv_resolver::{ExcludeNewer, FlatIndex};
use uv_settings::PythonInstallMirrors;
use uv_types::{AnyErrorBuild, BuildContext, BuildStack, HashStrategy, SourceTreeEditablePolicy};
use uv_workspace::pyproject::ExtraBuildDependencies;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError};
use crate::commands::ExitStatus;
use crate::commands::pip::operations;
use crate::commands::project::{ProjectError, find_requires_python};
use crate::commands::reporters::PythonDownloadReporter;
use crate::printer::Printer;
use crate::settings::ResolverSettings;
#[derive(Debug, Error)]
enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
FindOrDownloadPython(#[from] uv_python::Error),
#[error(transparent)]
HashStrategy(#[from] uv_types::HashStrategyError),
#[error(transparent)]
FlatIndex(#[from] uv_client::FlatIndexError),
#[error(transparent)]
ClientBuild(#[from] uv_client::ClientBuildError),
#[error(transparent)]
BuildPlan(anyhow::Error),
#[error(transparent)]
Extract(#[from] uv_extract::Error),
#[error(transparent)]
Operations(#[from] operations::Error),
#[error(transparent)]
Join(#[from] tokio::task::JoinError),
#[error(transparent)]
BuildBackend(#[from] uv_build_backend::Error),
#[error(transparent)]
BuildDispatch(AnyErrorBuild),
#[error(transparent)]
BuildFrontend(#[from] uv_build_frontend::Error),
#[error(transparent)]
Project(#[from] ProjectError),
#[error("Failed to write message")]
Fmt(#[from] fmt::Error),
#[error("Can't use `--force-pep517` with `--list`")]
ListForcePep517,
#[error(
"Can only use `--list` with a compatible uv build backend, but `{name}` is not compatible because {reason}"
)]
ListNonUv { name: String, reason: String },
#[error(
"`{0}` is not a valid build source. Expected to receive a source directory, or a source \
distribution ending in one of: {1}."
)]
InvalidSourceDistExt(String, uv_distribution_filename::ExtensionError),
#[error("The built source distribution has an invalid filename")]
InvalidBuiltSourceDistFilename(#[source] uv_distribution_filename::SourceDistFilenameError),
#[error("The built wheel has an invalid filename")]
InvalidBuiltWheelFilename(#[source] uv_distribution_filename::WheelFilenameError),
#[error("The source distribution declares version {0}, but the wheel declares version {1}")]
VersionMismatch(Version, Version),
}
#[expect(clippy::fn_params_excessive_bools)]
pub(crate) async fn build_frontend(
project_dir: &Path,
src: Option<PathBuf>,
package: Option<PackageName>,
all_packages: bool,
output_dir: Option<PathBuf>,
sdist: bool,
wheel: bool,
list: bool,
build_logs: bool,
gitignore: bool,
force_pep517: bool,
clear: bool,
build_constraints: Vec<RequirementsSource>,
build_constraints_from_workspace: Vec<Requirement>,
hash_checking: Option<HashCheckingMode>,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: &ResolverSettings,
client_builder: &BaseClientBuilder<'_>,
no_config: bool,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
concurrency: Concurrency,
cache: &Cache,
workspace_cache: &WorkspaceCache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
let build_result = build_impl(
project_dir,
src.as_deref(),
package.as_ref(),
all_packages,
output_dir.as_deref(),
sdist,
wheel,
list,
build_logs,
gitignore,
force_pep517,
clear,
&build_constraints,
&build_constraints_from_workspace,
hash_checking,
python.as_deref(),
install_mirrors,
settings,
client_builder,
no_config,
python_preference,
python_downloads,
&concurrency,
cache,
workspace_cache,
printer,
preview,
)
.await?;
match build_result {
BuildResult::Failure => Ok(ExitStatus::Error),
BuildResult::Success => Ok(ExitStatus::Success),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BuildResult {
Failure,
Success,
}
#[allow(unused_assignments)]
#[expect(clippy::fn_params_excessive_bools)]
async fn build_impl(
project_dir: &Path,
src: Option<&Path>,
package: Option<&PackageName>,
all_packages: bool,
output_dir: Option<&Path>,
sdist: bool,
wheel: bool,
list: bool,
build_logs: bool,
gitignore: bool,
force_pep517: bool,
clear: bool,
build_constraints: &[RequirementsSource],
build_constraints_from_workspace: &[Requirement],
hash_checking: Option<HashCheckingMode>,
python_request: Option<&str>,
install_mirrors: PythonInstallMirrors,
settings: &ResolverSettings,
client_builder: &BaseClientBuilder<'_>,
no_config: bool,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
concurrency: &Concurrency,
cache: &Cache,
workspace_cache: &WorkspaceCache,
printer: Printer,
preview: Preview,
) -> Result<BuildResult> {
let ResolverSettings {
index_locations,
index_strategy,
keyring_provider,
resolution: _,
prerelease: _,
fork_strategy: _,
dependency_metadata,
config_setting,
config_settings_package,
build_isolation,
extra_build_dependencies,
extra_build_variables,
exclude_newer,
link_mode,
upgrade: _,
build_options,
sources,
torch_backend: _,
} = settings;
let src = if let Some(src) = src {
let src = std::path::absolute(src)?;
let metadata = match fs_err::tokio::metadata(&src).await {
Ok(metadata) => metadata,
Err(err) if err.kind() == io::ErrorKind::NotFound => {
return Err(anyhow::anyhow!(
"Source `{}` does not exist",
src.user_display()
));
}
Err(err) => return Err(err.into()),
};
if metadata.is_file() {
Source::File(Cow::Owned(src))
} else {
Source::Directory(Cow::Owned(src))
}
} else {
Source::Directory(Cow::Borrowed(project_dir))
};
let workspace = Workspace::discover(
src.directory(),
&DiscoveryOptions::default(),
workspace_cache,
)
.await;
let packages = if let Some(package) = package {
if matches!(src, Source::File(_)) {
return Err(anyhow::anyhow!(
"Cannot specify `--package` when building from a file"
));
}
let workspace = match workspace {
Ok(ref workspace) => workspace,
Err(err) => {
return Err(err).context("`--package` was provided, but no workspace was found");
}
};
let package = workspace
.packages()
.get(package)
.ok_or_else(|| anyhow::anyhow!("Package `{package}` not found in workspace"))?;
if !package.pyproject_toml().is_package(true) {
let name = &package.project().name;
let pyproject_toml = package.root().join("pyproject.toml");
return Err(anyhow::anyhow!(
"Package `{}` is missing a `{}`. For example, to build with `{}`, add the following to `{}`:\n```toml\n[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n```",
name.cyan(),
"build-system".green(),
"setuptools".cyan(),
pyproject_toml.user_display().cyan()
));
}
vec![AnnotatedSource::from(Source::Directory(Cow::Borrowed(
package.root(),
)))]
} else if all_packages {
if matches!(src, Source::File(_)) {
return Err(anyhow::anyhow!(
"Cannot specify `--all-packages` when building from a file"
));
}
let workspace = match workspace {
Ok(ref workspace) => workspace,
Err(err) => {
return Err(err)
.context("`--all-packages` was provided, but no workspace was found");
}
};
if workspace.packages().is_empty() {
return Err(anyhow::anyhow!("No packages found in workspace"));
}
let packages: Vec<_> = workspace
.packages()
.values()
.filter(|package| package.pyproject_toml().is_package(true))
.map(|package| AnnotatedSource {
source: Source::Directory(Cow::Borrowed(package.root())),
package: Some(package.project().name.clone()),
})
.collect();
if packages.is_empty() {
let member = workspace.packages().values().next().unwrap();
let name = &member.project().name;
let pyproject_toml = member.root().join("pyproject.toml");
return Err(anyhow::anyhow!(
"Workspace does not contain any buildable packages. For example, to build `{}` with `{}`, add a `{}` to `{}`:\n```toml\n[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n```",
name.cyan(),
"setuptools".cyan(),
"build-system".green(),
pyproject_toml.user_display().cyan()
));
}
packages
} else {
vec![AnnotatedSource::from(src)]
};
let results: Vec<_> = futures::future::join_all(packages.into_iter().map(|source| {
let future = build_package(
source.clone(),
output_dir,
python_request,
install_mirrors.clone(),
no_config,
workspace.as_ref(),
python_preference,
python_downloads,
cache,
workspace_cache,
printer,
index_locations,
client_builder.clone(),
hash_checking,
build_logs,
gitignore,
force_pep517,
clear,
build_constraints,
build_constraints_from_workspace,
build_isolation,
extra_build_dependencies,
extra_build_variables,
*index_strategy,
*keyring_provider,
exclude_newer.clone(),
sources.clone(),
concurrency,
build_options,
sdist,
wheel,
list,
dependency_metadata,
*link_mode,
config_setting,
config_settings_package,
preview,
);
async {
let result = future.await;
(source, result)
}
}))
.await;
let mut success = true;
for (source, result) in results {
match result {
Ok(messages) => {
for message in messages {
message.print(printer)?;
}
}
Err(err) => {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("Failed to build `{source}`", source = source.cyan())]
#[diagnostic()]
struct Diagnostic {
source: String,
#[source]
cause: anyhow::Error,
#[help]
help: Option<String>,
}
let help = if let Error::Extract(uv_extract::Error::Tar(err)) = &err {
if err.to_string().contains("/bin/python")
&& std::error::Error::source(err).is_some_and(|err| {
let err = err.to_string();
err.ends_with("outside of the target directory")
|| err.ends_with("external symlinks are not allowed")
})
{
Some(
"This file seems to be part of a virtual environment. Virtual environments must be excluded from source distributions."
.to_string(),
)
} else {
None
}
} else {
None
};
let report = miette::Report::new(Diagnostic {
source: source.to_string(),
cause: err.into(),
help,
});
anstream::eprint!("{report:?}");
success = false;
}
}
}
if success {
Ok(BuildResult::Success)
} else {
Ok(BuildResult::Failure)
}
}
#[expect(clippy::fn_params_excessive_bools)]
async fn build_package(
source: AnnotatedSource<'_>,
output_dir: Option<&Path>,
python_request: Option<&str>,
install_mirrors: PythonInstallMirrors,
no_config: bool,
workspace: Result<&Workspace, &WorkspaceError>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
cache: &Cache,
workspace_cache: &WorkspaceCache,
printer: Printer,
index_locations: &IndexLocations,
client_builder: BaseClientBuilder<'_>,
hash_checking: Option<HashCheckingMode>,
build_logs: bool,
gitignore: bool,
force_pep517: bool,
clear: bool,
build_constraints: &[RequirementsSource],
build_constraints_from_workspace: &[Requirement],
build_isolation: &BuildIsolation,
extra_build_dependencies: &ExtraBuildDependencies,
extra_build_variables: &ExtraBuildVariables,
index_strategy: IndexStrategy,
keyring_provider: KeyringProviderType,
exclude_newer: ExcludeNewer,
sources: NoSources,
concurrency: &Concurrency,
build_options: &BuildOptions,
sdist: bool,
wheel: bool,
list: bool,
dependency_metadata: &DependencyMetadata,
link_mode: LinkMode,
config_setting: &ConfigSettings,
config_settings_package: &PackageConfigSettings,
preview: Preview,
) -> Result<Vec<BuildMessage>, Error> {
let output_dir = if let Some(output_dir) = output_dir {
Cow::Owned(std::path::absolute(output_dir)?)
} else {
if let Ok(workspace) = workspace {
Cow::Owned(workspace.install_path().join("dist"))
} else {
match &source.source {
Source::Directory(src) => Cow::Owned(src.join("dist")),
Source::File(src) => Cow::Borrowed(src.parent().unwrap()),
}
}
};
if clear && output_dir.exists() {
fs_err::remove_dir_all(&*output_dir)?;
}
let mut interpreter_request = python_request.map(PythonRequest::parse);
if interpreter_request.is_none() {
interpreter_request = PythonVersionFile::discover(
source.directory(),
&VersionFileDiscoveryOptions::default().with_no_config(no_config),
)
.await?
.and_then(PythonVersionFile::into_version);
}
if interpreter_request.is_none() {
if let Ok(workspace) = workspace {
let groups = DependencyGroupsWithDefaults::none();
interpreter_request = find_requires_python(workspace, &groups)?
.and_then(PythonRequest::from_requires_python);
}
}
let interpreter = PythonInstallation::find_or_download(
interpreter_request.as_ref(),
EnvironmentPreference::Any,
python_preference,
python_downloads,
&client_builder,
cache,
Some(&PythonDownloadReporter::single(printer)),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
preview,
)
.await?
.into_interpreter();
let build_constraints =
operations::read_constraints(build_constraints, &client_builder).await?;
let hasher = if let Some(hash_checking) = hash_checking {
HashStrategy::from_requirements(
std::iter::empty(),
build_constraints
.iter()
.map(|entry| (&entry.requirement, entry.hashes.as_slice())),
Some(&interpreter.resolver_marker_environment()),
hash_checking,
)?
} else {
HashStrategy::None
};
let build_constraints = Constraints::from_requirements(
build_constraints
.into_iter()
.map(|constraint| constraint.requirement)
.chain(build_constraints_from_workspace.iter().cloned()),
);
let client = RegistryClientBuilder::new(client_builder.clone(), cache.clone())
.index_locations(index_locations.clone())
.index_strategy(index_strategy)
.keyring(keyring_provider)
.markers(interpreter.markers())
.platform(interpreter.platform())
.build()?;
let environment;
let types_build_isolation = match build_isolation {
BuildIsolation::Isolate => uv_types::BuildIsolation::Isolated,
BuildIsolation::Shared => {
environment = PythonEnvironment::from_interpreter(interpreter.clone());
uv_types::BuildIsolation::Shared(&environment)
}
BuildIsolation::SharedPackage(packages) => {
environment = PythonEnvironment::from_interpreter(interpreter.clone());
uv_types::BuildIsolation::SharedPackage(&environment, packages)
}
};
let flat_index = {
let client = FlatIndexClient::new(client.cached_client(), client.connectivity(), cache);
let entries = client
.fetch_all(index_locations.flat_indexes().map(Index::url))
.await?;
FlatIndex::from_entries(entries, None, &hasher, build_options)
};
let state = SharedState::default();
let extra_build_requires =
LoweredExtraBuildDependencies::from_non_lowered(extra_build_dependencies.clone())
.into_inner();
let build_dispatch = BuildDispatch::new(
&client,
cache,
&build_constraints,
&interpreter,
index_locations,
&flat_index,
dependency_metadata,
state.clone(),
index_strategy,
config_setting,
config_settings_package,
types_build_isolation,
&extra_build_requires,
extra_build_variables,
link_mode,
build_options,
&hasher,
exclude_newer,
sources.clone(),
SourceTreeEditablePolicy::Project,
workspace_cache.clone(),
concurrency.clone(),
preview,
);
prepare_output_directory(&output_dir, gitignore).await?;
let plan = BuildPlan::determine(&source, sdist, wheel).map_err(Error::BuildPlan)?;
let build_action = if list {
if force_pep517 {
return Err(Error::ListForcePep517);
}
if let Err(reason) = check_direct_build(source.path(), uv_version::version()) {
return Err(Error::ListNonUv {
name: source.path().user_display().to_string(),
reason: reason.to_string(),
});
}
BuildAction::List
} else if force_pep517 {
BuildAction::Pep517
} else {
match check_direct_build(source.path(), uv_version::version()) {
Ok(()) => BuildAction::DirectBuild,
Err(reason) => {
debug!(
"Not using `uv_build` direct build for `{}` because {}",
source.path().user_display(),
reason
);
BuildAction::Pep517
}
}
};
let dist = None;
let subdirectory = None;
let version_id = source.path().file_name().and_then(|name| name.to_str());
let build_output = match printer {
Printer::Default | Printer::NoProgress | Printer::Verbose => {
if build_logs && !uv_flags::contains(uv_flags::EnvironmentFlags::HIDE_BUILD_OUTPUT) {
BuildOutput::Stderr
} else {
BuildOutput::Quiet
}
}
Printer::Quiet | Printer::Silent => BuildOutput::Quiet,
};
let mut build_results = Vec::new();
match plan {
BuildPlan::SdistToWheel => {
if list {
let sdist_list = build_sdist(
source.path(),
&output_dir,
build_action,
&source,
printer,
"source distribution",
&build_dispatch,
&sources,
dist,
subdirectory,
version_id,
build_output,
)
.await?;
build_results.push(sdist_list);
}
let sdist_build = build_sdist(
source.path(),
&output_dir,
build_action.force_build(),
&source,
printer,
"source distribution",
&build_dispatch,
&sources,
dist,
subdirectory,
version_id,
build_output,
)
.await?;
build_results.push(sdist_build.clone());
let path = output_dir.join(sdist_build.raw_filename());
let reader = fs_err::tokio::File::open(&path).await?;
let ext = SourceDistExtension::from_path(path.as_path())
.map_err(|err| Error::InvalidSourceDistExt(path.user_display().to_string(), err))?;
let temp_dir = tempfile::tempdir_in(cache.bucket(CacheBucket::SourceDistributions))?;
uv_extract::stream::archive(path.display(), reader, ext, temp_dir.path()).await?;
let extracted = match uv_extract::strip_component(temp_dir.path()) {
Ok(top_level) => top_level,
Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.path().to_path_buf(),
Err(err) => return Err(err.into()),
};
let wheel_build = build_wheel(
&extracted,
&output_dir,
build_action,
&source,
printer,
"wheel from source distribution",
&build_dispatch,
sources,
dist,
subdirectory,
version_id,
build_output,
Some(sdist_build.normalized_filename().version()),
)
.await?;
build_results.push(wheel_build);
}
BuildPlan::Sdist => {
let sdist_build = build_sdist(
source.path(),
&output_dir,
build_action,
&source,
printer,
"source distribution",
&build_dispatch,
&sources,
dist,
subdirectory,
version_id,
build_output,
)
.await?;
build_results.push(sdist_build);
}
BuildPlan::Wheel => {
let wheel_build = build_wheel(
source.path(),
&output_dir,
build_action,
&source,
printer,
"wheel",
&build_dispatch,
sources,
dist,
subdirectory,
version_id,
build_output,
None,
)
.await?;
build_results.push(wheel_build);
}
BuildPlan::SdistAndWheel => {
let sdist_build = build_sdist(
source.path(),
&output_dir,
build_action,
&source,
printer,
"source distribution",
&build_dispatch,
&sources,
dist,
subdirectory,
version_id,
build_output,
)
.await?;
let wheel_build = build_wheel(
source.path(),
&output_dir,
build_action,
&source,
printer,
"wheel",
&build_dispatch,
sources,
dist,
subdirectory,
version_id,
build_output,
Some(sdist_build.normalized_filename().version()),
)
.await?;
build_results.push(sdist_build);
build_results.push(wheel_build);
}
BuildPlan::WheelFromSdist => {
let reader = fs_err::tokio::File::open(source.path()).await?;
let ext = SourceDistExtension::from_path(source.path()).map_err(|err| {
Error::InvalidSourceDistExt(source.path().user_display().to_string(), err)
})?;
let temp_dir = tempfile::tempdir_in(&output_dir)?;
uv_extract::stream::archive(source.path().display(), reader, ext, temp_dir.path())
.await?;
let version = source
.path()
.file_name()
.and_then(|filename| filename.to_str())
.and_then(|filename| SourceDistFilename::parsed_normalized_filename(filename).ok())
.map(|filename| filename.version);
let extracted = match uv_extract::strip_component(temp_dir.path()) {
Ok(top_level) => top_level,
Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.path().to_path_buf(),
Err(err) => return Err(err.into()),
};
let wheel_build = build_wheel(
&extracted,
&output_dir,
build_action,
&source,
printer,
"wheel from source distribution",
&build_dispatch,
sources,
dist,
subdirectory,
version_id,
build_output,
version.as_ref(),
)
.await?;
build_results.push(wheel_build);
}
}
Ok(build_results)
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum BuildAction {
List,
DirectBuild,
Pep517,
}
impl BuildAction {
fn force_build(self) -> Self {
match self {
Self::List => Self::DirectBuild,
Self::DirectBuild => Self::DirectBuild,
Self::Pep517 => Self::Pep517,
}
}
}
#[instrument(skip_all)]
async fn build_sdist(
source_tree: &Path,
output_dir: &Path,
action: BuildAction,
source: &AnnotatedSource<'_>,
printer: Printer,
build_kind_message: &str,
build_dispatch: &BuildDispatch<'_>,
sources: &NoSources,
dist: Option<&SourceDist>,
subdirectory: Option<&Path>,
version_id: Option<&str>,
build_output: BuildOutput,
) -> Result<BuildMessage, Error> {
let build_result = match action {
BuildAction::List => {
let source_tree_ = source_tree.to_path_buf();
let sources_enabled = sources.is_none();
let (filename, file_list) = tokio::task::spawn_blocking(move || {
uv_build_backend::list_source_dist(
&source_tree_,
uv_version::version(),
sources_enabled,
)
})
.await??;
let raw_filename = filename.to_string();
BuildMessage::List {
normalized_filename: DistFilename::SourceDistFilename(filename),
raw_filename,
source_tree: source_tree.to_path_buf(),
file_list,
}
}
BuildAction::DirectBuild => {
writeln!(
printer.stderr(),
"{}",
format!(
"{}Building {} (uv build backend)...",
source.message_prefix(),
build_kind_message
)
.bold()
)?;
let source_tree = source_tree.to_path_buf();
let output_dir_ = output_dir.to_path_buf();
let sources_enabled = sources.is_none();
let filename = tokio::task::spawn_blocking(move || {
uv_build_backend::build_source_dist(
&source_tree,
&output_dir_,
uv_version::version(),
sources_enabled,
)
})
.await??
.to_string();
BuildMessage::Build {
normalized_filename: DistFilename::SourceDistFilename(
SourceDistFilename::parsed_normalized_filename(&filename)
.map_err(Error::InvalidBuiltSourceDistFilename)?,
),
raw_filename: filename,
output_dir: output_dir.to_path_buf(),
}
}
BuildAction::Pep517 => {
writeln!(
printer.stderr(),
"{}",
format!(
"{}Building {}...",
source.message_prefix(),
build_kind_message
)
.bold()
)?;
let builder = build_dispatch
.setup_build(
source_tree,
subdirectory,
source.path(),
version_id,
dist,
sources,
BuildKind::Sdist,
build_output,
BuildStack::default(),
)
.await
.map_err(|err| Error::BuildDispatch(err.into()))?;
let filename = builder.build(output_dir).await?;
BuildMessage::Build {
normalized_filename: DistFilename::SourceDistFilename(
SourceDistFilename::parsed_normalized_filename(&filename)
.map_err(Error::InvalidBuiltSourceDistFilename)?,
),
raw_filename: filename,
output_dir: output_dir.to_path_buf(),
}
}
};
Ok(build_result)
}
#[instrument(skip_all)]
async fn build_wheel(
source_tree: &Path,
output_dir: &Path,
action: BuildAction,
source: &AnnotatedSource<'_>,
printer: Printer,
build_kind_message: &str,
build_dispatch: &BuildDispatch<'_>,
sources: NoSources,
dist: Option<&SourceDist>,
subdirectory: Option<&Path>,
version_id: Option<&str>,
build_output: BuildOutput,
version: Option<&Version>,
) -> Result<BuildMessage, Error> {
let build_message = match action {
BuildAction::List => {
let source_tree_ = source_tree.to_path_buf();
let sources_enabled = sources.is_none();
let (filename, file_list) = tokio::task::spawn_blocking(move || {
uv_build_backend::list_wheel(&source_tree_, uv_version::version(), sources_enabled)
})
.await??;
let raw_filename = filename.to_string();
BuildMessage::List {
normalized_filename: DistFilename::WheelFilename(filename),
raw_filename,
source_tree: source_tree.to_path_buf(),
file_list,
}
}
BuildAction::DirectBuild => {
writeln!(
printer.stderr(),
"{}",
format!(
"{}Building {} (uv build backend)...",
source.message_prefix(),
build_kind_message
)
.bold()
)?;
let source_tree = source_tree.to_path_buf();
let output_dir_ = output_dir.to_path_buf();
let sources_enabled = sources.is_none();
let filename = tokio::task::spawn_blocking(move || {
uv_build_backend::build_wheel(
&source_tree,
&output_dir_,
None,
uv_version::version(),
sources_enabled,
)
})
.await??;
let raw_filename = filename.to_string();
BuildMessage::Build {
normalized_filename: DistFilename::WheelFilename(filename),
raw_filename,
output_dir: output_dir.to_path_buf(),
}
}
BuildAction::Pep517 => {
writeln!(
printer.stderr(),
"{}",
format!(
"{}Building {}...",
source.message_prefix(),
build_kind_message
)
.bold()
)?;
let builder = build_dispatch
.setup_build(
source_tree,
subdirectory,
source.path(),
version_id,
dist,
&sources,
BuildKind::Wheel,
build_output,
BuildStack::default(),
)
.await
.map_err(|err| Error::BuildDispatch(err.into()))?;
let filename = builder.build(output_dir).await?;
BuildMessage::Build {
normalized_filename: DistFilename::WheelFilename(
WheelFilename::from_str(&filename).map_err(Error::InvalidBuiltWheelFilename)?,
),
raw_filename: filename,
output_dir: output_dir.to_path_buf(),
}
}
};
if let Some(expected) = version {
let actual = build_message.normalized_filename().version();
if expected != actual {
return Err(Error::VersionMismatch(expected.clone(), actual.clone()));
}
}
Ok(build_message)
}
async fn prepare_output_directory(output_dir: &Path, gitignore: bool) -> Result<(), Error> {
fs_err::tokio::create_dir_all(&output_dir).await?;
if 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> {
source: Source<'a>,
package: Option<PackageName>,
}
impl AnnotatedSource<'_> {
fn path(&self) -> &Path {
self.source.path()
}
fn directory(&self) -> &Path {
self.source.directory()
}
fn message_prefix(&self) -> Cow<'_, str> {
if let Some(package) = &self.package {
Cow::Owned(format!("[{}] ", package.cyan()))
} else {
Cow::Borrowed("")
}
}
}
impl<'a> From<Source<'a>> for AnnotatedSource<'a> {
fn from(source: Source<'a>) -> Self {
Self {
source,
package: None,
}
}
}
impl fmt::Display for AnnotatedSource<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(package) = &self.package {
write!(f, "{} @ {}", package, self.path().simplified_display())
} else {
write!(f, "{}", self.path().simplified_display())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Source<'a> {
File(Cow<'a, Path>),
Directory(Cow<'a, Path>),
}
impl Source<'_> {
fn path(&self) -> &Path {
match self {
Self::File(path) => path.as_ref(),
Self::Directory(path) => path.as_ref(),
}
}
fn directory(&self) -> &Path {
match self {
Self::File(path) => path.parent().unwrap(),
Self::Directory(path) => path,
}
}
}
#[derive(Debug, Clone)]
enum BuildMessage {
Build {
normalized_filename: DistFilename,
raw_filename: String,
output_dir: PathBuf,
},
List {
normalized_filename: DistFilename,
raw_filename: String,
source_tree: PathBuf,
file_list: Vec<(String, Option<PathBuf>)>,
},
}
impl BuildMessage {
fn normalized_filename(&self) -> &DistFilename {
match self {
Self::Build {
normalized_filename: name,
..
} => name,
Self::List {
normalized_filename: name,
..
} => name,
}
}
fn raw_filename(&self) -> &str {
match self {
Self::Build {
raw_filename: name, ..
} => name,
Self::List {
raw_filename: name, ..
} => name,
}
}
fn print(&self, printer: Printer) -> Result<()> {
match self {
Self::Build {
raw_filename,
output_dir,
..
} => {
writeln!(
printer.stderr(),
"Successfully built {}",
output_dir.join(raw_filename).user_display().bold().cyan()
)?;
}
Self::List {
raw_filename,
file_list,
source_tree,
..
} => {
writeln!(
printer.stdout(),
"{}",
format!("Building {raw_filename} will include the following files:").bold()
)?;
for (file, source) in file_list {
if let Some(source) = source {
writeln!(
printer.stdout(),
"{file} ({})",
relative_to(source, source_tree)
.context("Included files must be relative to source tree")?
.display()
)?;
} else {
writeln!(printer.stdout(), "{file} (generated)")?;
}
}
}
}
Ok(())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum BuildPlan {
SdistToWheel,
Sdist,
Wheel,
SdistAndWheel,
WheelFromSdist,
}
impl BuildPlan {
fn determine(source: &AnnotatedSource, sdist: bool, wheel: bool) -> Result<Self> {
Ok(match &source.source {
Source::File(_) => {
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(_) => {
match (sdist, wheel) {
(false, false) => Self::SdistToWheel,
(false, true) => Self::Wheel,
(true, false) => Self::Sdist,
(true, true) => Self::SdistAndWheel,
}
}
})
}
}