From 76ad85a6905899474a7d8b2c1ef8b28696dea95f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 31 Aug 2024 13:42:01 -0400 Subject: [PATCH] Add uv build --- Cargo.lock | 10 +- crates/uv-build/src/lib.rs | 2 +- crates/uv-cli/src/lib.rs | 86 ++++++-- crates/uv-cli/src/options.rs | 18 +- crates/uv-dev/Cargo.toml | 10 +- crates/uv-dev/src/build.rs | 114 ----------- crates/uv-dev/src/main.rs | 10 +- crates/uv/Cargo.toml | 3 + crates/uv/src/commands/build.rs | 338 ++++++++++++++++++++++++++++++++ crates/uv/src/commands/mod.rs | 2 + crates/uv/src/lib.rs | 33 +++- crates/uv/src/settings.rs | 40 +++- crates/uv/tests/build.rs | 171 ++++++++++++++++ crates/uv/tests/common/mod.rs | 8 + crates/uv/tests/help.rs | 7 + docs/reference/cli.md | 248 ++++++++++++++++++++++- 16 files changed, 932 insertions(+), 168 deletions(-) delete mode 100644 crates/uv-dev/src/build.rs create mode 100644 crates/uv/src/commands/build.rs create mode 100644 crates/uv/tests/build.rs diff --git a/Cargo.lock b/Cargo.lock index 11f640bbd7af..78d091d5e9f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4463,6 +4463,7 @@ dependencies = [ "byteorder", "cache-key", "clap", + "distribution-filename", "distribution-types", "etcetera", "filetime", @@ -4511,6 +4512,7 @@ dependencies = [ "uv-configuration", "uv-dispatch", "uv-distribution", + "uv-extract", "uv-fs", "uv-git", "uv-installer", @@ -4715,7 +4717,6 @@ dependencies = [ "distribution-filename", "distribution-types", "fs-err", - "install-wheel-rs", "itertools 0.13.0", "markdown", "mimalloc", @@ -4725,7 +4726,6 @@ dependencies = [ "pretty_assertions", "pypi-types", "resvg", - "rustc-hash", "schemars", "serde", "serde_json", @@ -4736,20 +4736,14 @@ dependencies = [ "tracing", "tracing-durations-export", "tracing-subscriber", - "uv-build", "uv-cache", "uv-cli", "uv-client", - "uv-configuration", - "uv-dispatch", - "uv-git", "uv-installer", "uv-macros", "uv-options-metadata", "uv-python", - "uv-resolver", "uv-settings", - "uv-types", "uv-workspace", "walkdir", ] diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index bf76c458f8cf..bf797b9bdde3 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -79,7 +79,7 @@ static DEFAULT_BACKEND: LazyLock = LazyLock::new(|| Pep517Backend pub enum Error { #[error(transparent)] Io(#[from] io::Error), - #[error("{} does not appear to be a Python project, as neither `pyproject.toml` nor `setup.py` is present in the directory", _0.simplified_display())] + #[error("{} does not appear to be a Python project, as neither `pyproject.toml` nor `setup.py` are present in the directory", _0.simplified_display())] InvalidSourceDist(PathBuf), #[error("Invalid `pyproject.toml`")] InvalidPyprojectToml(#[from] toml::de::Error), diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index c6131cc8eae7..ca02ce0685f5 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -335,6 +335,21 @@ pub enum Commands { after_long_help = "" )] Venv(VenvArgs), + /// Build Python packages into source distributions and wheels. + /// + /// By default, `uv build` will build a source distribution ("sdist") + /// from the source directory, and a binary distribution ("wheel") from + /// the source distribution. + /// + /// `uv build --sdist` can be used to build only the source distribution, + /// `uv build --wheel` can be used to build only the binary distribution, + /// and `uv build --sdist --wheel` can be used to build both distributions + /// from source. + #[command( + after_help = "Use `uv help build` for more details.", + after_long_help = "" + )] + Build(BuildArgs), /// Manage uv's cache. #[command( after_help = "Use `uv help cache` for more details.", @@ -1125,7 +1140,7 @@ pub struct PipSyncArgs { /// The Python interpreter into which packages should be installed. /// - /// By default, syncing requires a virtual environment. An path to an + /// By default, syncing requires a virtual environment. A path to an /// alternative Python can be provided, but it is only recommended in /// continuous integration (CI) environments and should be used with /// caution, as it can modify the system Python installation. @@ -1407,7 +1422,7 @@ pub struct PipInstallArgs { /// The Python interpreter into which packages should be installed. /// - /// By default, installation requires a virtual environment. An path to an + /// By default, installation requires a virtual environment. A path to an /// alternative Python can be provided, but it is only recommended in /// continuous integration (CI) environments and should be used with /// caution, as it can modify the system Python installation. @@ -1572,7 +1587,7 @@ pub struct PipUninstallArgs { /// The Python interpreter from which packages should be uninstalled. /// - /// By default, uninstallation requires a virtual environment. An path to an + /// By default, uninstallation requires a virtual environment. A path to an /// alternative Python can be provided, but it is only recommended in /// continuous integration (CI) environments and should be used with /// caution, as it can modify the system Python installation. @@ -1923,6 +1938,49 @@ pub struct PipTreeArgs { pub compat_args: compat::PipGlobalCompatArgs, } +#[derive(Args)] +#[allow(clippy::struct_excessive_bools)] +pub struct BuildArgs { + /// The directory from which source distributions and/or wheels should be built. + /// + /// Defaults to the current working directory. + #[arg(value_parser = parse_file_path)] + pub src_dir: Option, + + /// Build a source distribution ("sdist") from the given directory. + #[arg(long)] + pub sdist: bool, + + /// Build a built distribution ("wheel") from the given directory. + #[arg(long)] + pub wheel: bool, + + /// The Python interpreter to use for the build environment. + /// + /// By default, builds are executed in isolated virtual environments. The + /// discovered interpreter will be used to create those environments, and + /// will be symlinked or copied in depending on the platform. + /// + /// See `uv help python` to view supported request formats. + #[arg( + long, + short, + env = "UV_PYTHON", + verbatim_doc_comment, + help_heading = "Python options" + )] + pub python: Option, + + #[command(flatten)] + pub resolver: ResolverArgs, + + #[command(flatten)] + pub build: BuildOptionsArgs, + + #[command(flatten)] + pub refresh: RefreshArgs, +} + #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct VenvArgs { @@ -2298,7 +2356,7 @@ pub struct RunArgs { pub installer: ResolverInstallerArgs, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, @@ -2426,7 +2484,7 @@ pub struct SyncArgs { pub installer: ResolverInstallerArgs, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, @@ -2479,7 +2537,7 @@ pub struct LockArgs { pub resolver: ResolverArgs, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, @@ -2593,7 +2651,7 @@ pub struct AddArgs { pub installer: ResolverInstallerArgs, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, @@ -2662,7 +2720,7 @@ pub struct RemoveArgs { pub installer: ResolverInstallerArgs, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, @@ -2722,7 +2780,7 @@ pub struct TreeArgs { pub frozen: bool, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, #[command(flatten)] pub resolver: ResolverArgs, @@ -2819,7 +2877,7 @@ pub struct ExportArgs { pub resolver: ResolverArgs, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, @@ -2966,7 +3024,7 @@ pub struct ToolRunArgs { pub installer: ResolverInstallerArgs, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, @@ -3018,7 +3076,7 @@ pub struct ToolInstallArgs { pub installer: ResolverInstallerArgs, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, #[command(flatten)] pub refresh: RefreshArgs, @@ -3103,7 +3161,7 @@ pub struct ToolUpgradeArgs { pub installer: ResolverInstallerArgs, #[command(flatten)] - pub build: BuildArgs, + pub build: BuildOptionsArgs, } #[derive(Args)] @@ -3407,7 +3465,7 @@ pub struct RefreshArgs { #[derive(Args)] #[allow(clippy::struct_excessive_bools)] -pub struct BuildArgs { +pub struct BuildOptionsArgs { /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary Python code. The cached wheels of diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index 6fb230b23498..dc66217124c0 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -4,7 +4,8 @@ use uv_resolver::PrereleaseMode; use uv_settings::{PipOptions, ResolverInstallerOptions, ResolverOptions}; use crate::{ - BuildArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs, ResolverInstallerArgs, + BuildOptionsArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs, + ResolverInstallerArgs, }; /// Given a boolean flag pair (like `--upgrade` and `--no-upgrade`), resolve the value of the flag. @@ -206,8 +207,11 @@ impl From for PipOptions { } } -/// Construct the [`ResolverOptions`] from the [`ResolverArgs`] and [`BuildArgs`]. -pub fn resolver_options(resolver_args: ResolverArgs, build_args: BuildArgs) -> ResolverOptions { +/// Construct the [`ResolverOptions`] from the [`ResolverArgs`] and [`BuildOptionsArgs`]. +pub fn resolver_options( + resolver_args: ResolverArgs, + build_args: BuildOptionsArgs, +) -> ResolverOptions { let ResolverArgs { index_args, upgrade, @@ -228,7 +232,7 @@ pub fn resolver_options(resolver_args: ResolverArgs, build_args: BuildArgs) -> R no_sources, } = resolver_args; - let BuildArgs { + let BuildOptionsArgs { no_build, build, no_build_package, @@ -281,10 +285,10 @@ pub fn resolver_options(resolver_args: ResolverArgs, build_args: BuildArgs) -> R } } -/// Construct the [`ResolverInstallerOptions`] from the [`ResolverInstallerArgs`] and [`BuildArgs`]. +/// Construct the [`ResolverInstallerOptions`] from the [`ResolverInstallerArgs`] and [`BuildOptionsArgs`]. pub fn resolver_installer_options( resolver_installer_args: ResolverInstallerArgs, - build_args: BuildArgs, + build_args: BuildOptionsArgs, ) -> ResolverInstallerOptions { let ResolverInstallerArgs { index_args, @@ -311,7 +315,7 @@ pub fn resolver_installer_options( no_sources, } = resolver_installer_args; - let BuildArgs { + let BuildOptionsArgs { no_build, build, no_build_package, diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index a7a740a6de6a..d7cef9f7823c 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -18,23 +18,16 @@ workspace = true [dependencies] distribution-filename = { workspace = true } distribution-types = { workspace = true } -install-wheel-rs = { workspace = true } pep508_rs = { workspace = true } pypi-types = { workspace = true } -uv-build = { workspace = true } uv-cache = { workspace = true, features = ["clap"] } uv-cli = { workspace = true } uv-client = { workspace = true } -uv-configuration = { workspace = true } -uv-dispatch = { workspace = true } -uv-git = { workspace = true } uv-installer = { workspace = true } uv-macros = { workspace = true } uv-options-metadata = { workspace = true } uv-python = { workspace = true } -uv-resolver = { workspace = true } uv-settings = { workspace = true, features = ["schemars"] } -uv-types = { workspace = true } uv-workspace = { workspace = true, features = ["schemars"] } # Any dependencies that are exclusively used in `uv-dev` should be listed as non-workspace @@ -44,12 +37,11 @@ anyhow = { workspace = true } clap = { workspace = true, features = ["derive", "wrap_help"] } fs-err = { workspace = true, features = ["tokio"] } itertools = { workspace = true } -markdown = "0.3.0" +markdown = { version = "0.3.0" } owo-colors = { workspace = true } poloto = { version = "19.1.2", optional = true } pretty_assertions = { version = "1.4.0" } resvg = { version = "0.29.0", optional = true } -rustc-hash = { workspace = true } schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/uv-dev/src/build.rs b/crates/uv-dev/src/build.rs deleted file mode 100644 index 278d90dec6c8..000000000000 --- a/crates/uv-dev/src/build.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::env; -use std::path::PathBuf; - -use anyhow::{Context, Result}; -use clap::Parser; -use fs_err as fs; -use rustc_hash::FxHashMap; - -use distribution_types::IndexLocations; -use uv_build::{SourceBuild, SourceBuildContext}; -use uv_cache::{Cache, CacheArgs}; -use uv_client::RegistryClientBuilder; -use uv_configuration::{ - BuildKind, BuildOptions, Concurrency, ConfigSettings, IndexStrategy, SourceStrategy, -}; -use uv_dispatch::BuildDispatch; -use uv_git::GitResolver; -use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; -use uv_resolver::{FlatIndex, InMemoryIndex}; -use uv_types::{BuildIsolation, InFlight}; - -#[derive(Parser)] -pub(crate) struct BuildArgs { - /// Base python in a way that can be found with `which` - /// TODO(konstin): Also use proper python parsing here - #[clap(short, long)] - python: Option, - /// Directory to story the built wheel in - #[clap(short, long)] - wheels: Option, - /// The source distribution to build, as a directory. - sdist: PathBuf, - /// The subdirectory to build within the source distribution. - subdirectory: Option, - /// You can edit the python sources of an editable install and the changes will be used without - /// the need to reinstall it. - #[clap(short, long)] - editable: bool, - #[command(flatten)] - cache_args: CacheArgs, -} - -/// Build a source distribution to a wheel -pub(crate) async fn build(args: BuildArgs) -> Result { - let wheel_dir = if let Some(wheel_dir) = args.wheels { - fs::create_dir_all(&wheel_dir).context("Invalid wheel directory")?; - wheel_dir - } else { - env::current_dir()? - }; - let build_kind = if args.editable { - BuildKind::Editable - } else { - BuildKind::Wheel - }; - - let cache = Cache::try_from(args.cache_args)?.init()?; - - let client = RegistryClientBuilder::new(cache.clone()).build(); - let concurrency = Concurrency::default(); - let config_settings = ConfigSettings::default(); - let exclude_newer = None; - let flat_index = FlatIndex::default(); - let git = GitResolver::default(); - let in_flight = InFlight::default(); - let index = InMemoryIndex::default(); - let index_urls = IndexLocations::default(); - let index_strategy = IndexStrategy::default(); - let sources = SourceStrategy::default(); - let python = PythonEnvironment::find( - &PythonRequest::default(), - EnvironmentPreference::OnlyVirtual, - &cache, - )?; - let build_options = BuildOptions::default(); - let build_constraints = []; - - let build_dispatch = BuildDispatch::new( - &client, - &cache, - &build_constraints, - python.interpreter(), - &index_urls, - &flat_index, - &index, - &git, - &in_flight, - index_strategy, - &config_settings, - BuildIsolation::Isolated, - install_wheel_rs::linker::LinkMode::default(), - &build_options, - exclude_newer, - sources, - concurrency, - ); - - let builder = SourceBuild::setup( - &args.sdist, - args.subdirectory.as_deref(), - None, - python.interpreter(), - &build_dispatch, - SourceBuildContext::default(), - args.sdist.display().to_string(), - config_settings.clone(), - BuildIsolation::Isolated, - build_kind, - FxHashMap::default(), - concurrency.builds, - ) - .await?; - Ok(wheel_dir.join(builder.build(&wheel_dir).await?)) -} diff --git a/crates/uv-dev/src/main.rs b/crates/uv-dev/src/main.rs index 7530167bee77..4ee22e757251 100644 --- a/crates/uv-dev/src/main.rs +++ b/crates/uv-dev/src/main.rs @@ -4,7 +4,7 @@ use std::process::ExitCode; use std::str::FromStr; use std::time::Instant; -use anstream::{eprintln, println}; +use anstream::eprintln; use anyhow::Result; use clap::Parser; use owo_colors::OwoColorize; @@ -16,7 +16,6 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, Layer}; -use crate::build::{build, BuildArgs}; use crate::clear_compile::ClearCompileArgs; use crate::compile::CompileArgs; use crate::generate_all::Args as GenerateAllArgs; @@ -43,7 +42,6 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; #[global_allocator] static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; -mod build; mod clear_compile; mod compile; mod generate_all; @@ -57,8 +55,6 @@ const ROOT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../"); #[derive(Parser)] enum Cli { - /// Build a source distribution into a wheel. - Build(BuildArgs), /// Display the metadata for a `.whl` at a given URL. WheelMetadata(WheelMetadataArgs), /// Compile all `.py` to `.pyc` files in the tree. @@ -82,10 +78,6 @@ enum Cli { async fn run() -> Result<()> { let cli = Cli::parse(); match cli { - Cli::Build(args) => { - let target = build(args).await?; - println!("Wheel built to {}", target.display()); - } Cli::WheelMetadata(args) => wheel_metadata::wheel_metadata(args).await?, Cli::Compile(args) => compile::compile(args).await?, Cli::ClearCompile(args) => clear_compile::clear_compile(&args)?, diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index f4f2f7df9644..d97150f9f0a4 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] cache-key = { workspace = true } +distribution-filename = { workspace = true } distribution-types = { workspace = true } install-wheel-rs = { workspace = true, default-features = false } pep440_rs = { workspace = true } @@ -28,6 +29,7 @@ uv-client = { workspace = true } uv-configuration = { workspace = true } uv-dispatch = { workspace = true } uv-distribution = { workspace = true } +uv-extract = { workspace = true } uv-fs = { workspace = true } uv-git = { workspace = true } uv-installer = { workspace = true } @@ -63,6 +65,7 @@ regex = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +tempfile = { workspace = true } textwrap = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/crates/uv/src/commands/build.rs b/crates/uv/src/commands/build.rs new file mode 100644 index 000000000000..198ce835a5d4 --- /dev/null +++ b/crates/uv/src/commands/build.rs @@ -0,0 +1,338 @@ +use crate::commands::project::find_requires_python; +use crate::commands::reporters::PythonDownloadReporter; +use crate::commands::{ExitStatus, SharedState}; +use crate::printer::Printer; +use crate::settings::{ResolverSettings, ResolverSettingsRef}; + +use anyhow::Result; +use distribution_filename::SourceDistExtension; +use owo_colors::OwoColorize; +use std::path::{Path, PathBuf}; +use uv_auth::store_credentials_from_url; +use uv_cache::Cache; +use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; +use uv_configuration::{BuildKind, Concurrency}; +use uv_dispatch::BuildDispatch; +use uv_fs::{Simplified, CWD}; +use uv_python::{ + EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation, + PythonPreference, PythonRequest, PythonVersionFile, VersionRequest, +}; +use uv_resolver::{FlatIndex, RequiresPython}; +use uv_types::{BuildContext, BuildIsolation, HashStrategy}; +use uv_warnings::warn_user_once; +use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError}; + +/// Build source distributions and wheels. +#[allow(clippy::fn_params_excessive_bools)] +pub(crate) async fn build( + path: Option, + sdist: bool, + wheel: bool, + python: Option, + settings: ResolverSettings, + no_config: bool, + python_preference: PythonPreference, + python_downloads: PythonDownloads, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, + cache: &Cache, + printer: Printer, +) -> Result { + let assets = build_impl( + path.as_deref(), + sdist, + wheel, + python.as_deref(), + settings.as_ref(), + no_config, + python_preference, + python_downloads, + connectivity, + concurrency, + native_tls, + cache, + printer, + ) + .await?; + + match assets { + BuiltDistributions::Wheel(wheel) => { + anstream::eprintln!("Successfully built {}", wheel.bold().cyan()); + } + BuiltDistributions::Sdist(sdist) => { + anstream::eprintln!("Successfully built {}", sdist.bold().cyan()); + } + BuiltDistributions::Both(sdist, wheel) => { + anstream::eprintln!( + "Successfully built {} and {}", + sdist.bold().cyan(), + wheel.bold().cyan() + ); + } + } + + Ok(ExitStatus::Success) +} + +#[allow(clippy::fn_params_excessive_bools)] +async fn build_impl( + path: Option<&Path>, + sdist: bool, + wheel: bool, + python_request: Option<&str>, + settings: ResolverSettingsRef<'_>, + no_config: bool, + python_preference: PythonPreference, + python_downloads: PythonDownloads, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, + cache: &Cache, + printer: Printer, +) -> Result { + // Extract the resolver settings. + let ResolverSettingsRef { + index_locations, + index_strategy, + keyring_provider, + allow_insecure_host, + resolution: _, + prerelease: _, + config_setting, + no_build_isolation, + no_build_isolation_package, + exclude_newer, + link_mode, + upgrade: _, + build_options, + sources, + } = settings; + + let client_builder = BaseClientBuilder::default() + .connectivity(connectivity) + .native_tls(native_tls); + + // (1) Explicit request from user + let mut interpreter_request = python_request.map(PythonRequest::parse); + + // (2) Request from `.python-version` + if interpreter_request.is_none() { + interpreter_request = PythonVersionFile::discover(&*CWD, no_config, false) + .await? + .and_then(PythonVersionFile::into_version); + } + + // (3) `Requires-Python` in `pyproject.toml` + if interpreter_request.is_none() { + let project = match VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await { + Ok(project) => Some(project), + Err(WorkspaceError::MissingProject(_)) => None, + Err(WorkspaceError::MissingPyprojectToml) => None, + Err(WorkspaceError::NonWorkspace(_)) => None, + Err(err) => { + warn_user_once!("{err}"); + None + } + }; + + if let Some(project) = project { + interpreter_request = find_requires_python(project.workspace())? + .as_ref() + .map(RequiresPython::specifiers) + .map(|specifiers| { + PythonRequest::Version(VersionRequest::Range(specifiers.clone())) + }); + } + } + + // Locate the Python interpreter to use in the environment. + let interpreter = PythonInstallation::find_or_download( + interpreter_request.as_ref(), + EnvironmentPreference::Any, + python_preference, + python_downloads, + &client_builder, + cache, + Some(&PythonDownloadReporter::single(printer)), + ) + .await? + .into_interpreter(); + + // Add all authenticated sources to the cache. + for url in index_locations.urls() { + store_credentials_from_url(url); + } + + // Initialize the registry client. + let client = RegistryClientBuilder::new(cache.clone()) + .native_tls(native_tls) + .connectivity(connectivity) + .index_urls(index_locations.index_urls()) + .index_strategy(index_strategy) + .keyring(keyring_provider) + .allow_insecure_host(allow_insecure_host.to_vec()) + .markers(interpreter.markers()) + .platform(interpreter.platform()) + .build(); + + // Determine whether to enable build isolation. + let environment; + let build_isolation = if no_build_isolation { + environment = PythonEnvironment::from_interpreter(interpreter.clone()); + BuildIsolation::Shared(&environment) + } else if no_build_isolation_package.is_empty() { + BuildIsolation::Isolated + } else { + environment = PythonEnvironment::from_interpreter(interpreter.clone()); + BuildIsolation::SharedPackage(&environment, no_build_isolation_package) + }; + + // TODO(charlie): These are all default values. We should consider whether we want to make them + // optional on the downstream APIs. + let build_constraints = []; + let hasher = HashStrategy::None; + + // Resolve the flat indexes from `--find-links`. + let flat_index = { + let client = FlatIndexClient::new(&client, cache); + let entries = client.fetch(index_locations.flat_index()).await?; + FlatIndex::from_entries(entries, None, &hasher, build_options) + }; + + // Initialize any shared state. + let state = SharedState::default(); + + // Create a build dispatch. + let build_dispatch = BuildDispatch::new( + &client, + cache, + &build_constraints, + &interpreter, + index_locations, + &flat_index, + &state.index, + &state.git, + &state.in_flight, + index_strategy, + config_setting, + build_isolation, + link_mode, + build_options, + exclude_newer, + sources, + concurrency, + ); + + // Determine the build plan from the command-line arguments. + let path = path.unwrap_or_else(|| &*CWD); + let output_dir = path.join("dist"); + fs_err::tokio::create_dir_all(&output_dir).await?; + + let plan = match (sdist, wheel) { + (false, false) => BuildPlan::SdistToWheel, + (true, false) => BuildPlan::Sdist, + (false, true) => BuildPlan::Wheel, + (true, true) => BuildPlan::SdistAndWheel, + }; + + // Prepare some common arguments for the build. + let subdirectory = None; + let version_id = path.user_display().to_string(); + let dist = None; + + let assets = match plan { + BuildPlan::SdistToWheel => { + // Build the sdist. + let builder = build_dispatch + .setup_build(path, subdirectory, &version_id, dist, BuildKind::Sdist) + .await?; + let sdist = builder.build(&output_dir).await?; + + // Extract the source distribution into a temporary directory. + let path = output_dir.join(&sdist); + let reader = fs_err::tokio::File::open(&path).await?; + let ext = SourceDistExtension::from_path(&path)?; + let temp_dir = tempfile::tempdir_in(&output_dir)?; + uv_extract::stream::archive(reader, ext, temp_dir.path()).await?; + + // Extract the top-level directory from the archive. + 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()), + }; + + // Build a wheel from the source distribution. + let builder = build_dispatch + .setup_build( + &extracted, + subdirectory, + &version_id, + dist, + BuildKind::Wheel, + ) + .await?; + let wheel = builder.build(&output_dir).await?; + + BuiltDistributions::Both(sdist, wheel) + } + BuildPlan::Sdist => { + let builder = build_dispatch + .setup_build(path, subdirectory, &version_id, dist, BuildKind::Sdist) + .await?; + let sdist = builder.build(&output_dir).await?; + + BuiltDistributions::Sdist(sdist) + } + BuildPlan::Wheel => { + let builder = build_dispatch + .setup_build(path, subdirectory, &version_id, dist, BuildKind::Wheel) + .await?; + let wheel = builder.build(&output_dir).await?; + + BuiltDistributions::Wheel(wheel) + } + BuildPlan::SdistAndWheel => { + let builder = build_dispatch + .setup_build(path, subdirectory, &version_id, dist, BuildKind::Sdist) + .await?; + let sdist = builder.build(&output_dir).await?; + + let builder = build_dispatch + .setup_build(path, subdirectory, &version_id, dist, BuildKind::Wheel) + .await?; + let wheel = builder.build(&output_dir).await?; + + BuiltDistributions::Both(sdist, wheel) + } + }; + + Ok(assets) +} + +#[derive(Debug, Clone, PartialEq)] +enum BuiltDistributions { + /// A built wheel. + Wheel(String), + /// A built source distribution. + Sdist(String), + /// A built source distribution and wheel. + Both(String, String), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum BuildPlan { + /// Build a source distribution from source, then build the wheel from the source distribution. + SdistToWheel, + + /// Build a source distribution from source. + Sdist, + + /// Build a wheel from source. + Wheel, + + /// Build a source distribution and a wheel from source. + SdistAndWheel, +} diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index d013d48b5e65..f228e644dee4 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -4,6 +4,7 @@ use std::{fmt::Display, fmt::Write, process::ExitCode}; use anyhow::Context; use owo_colors::OwoColorize; +pub(crate) use build::build; pub(crate) use cache_clean::cache_clean; pub(crate) use cache_dir::cache_dir; pub(crate) use cache_prune::cache_prune; @@ -65,6 +66,7 @@ mod python; pub(crate) mod reporters; mod tool; +mod build; #[cfg(feature = "self-update")] mod self_update; mod venv; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e9535db71d37..934c985c27e4 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -660,6 +660,34 @@ async fn run(cli: Cli) -> Result { commands::cache_dir(&cache); Ok(ExitStatus::Success) } + Commands::Build(args) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::BuildSettings::resolve(args, filesystem); + show_settings!(args); + + // Initialize the cache. + let cache = cache.init()?.with_refresh( + args.refresh + .combine(Refresh::from(args.settings.upgrade.clone())), + ); + + commands::build( + args.src_dir, + args.sdist, + args.wheel, + args.python, + args.settings, + cli.no_config, + globals.python_preference, + globals.python_downloads, + globals.connectivity, + globals.concurrency, + globals.native_tls, + &cache, + printer, + ) + .await + } Commands::Venv(args) => { args.compat_args.validate()?; @@ -1133,7 +1161,10 @@ async fn run_project( show_settings!(args); // Initialize the cache. - let cache = cache.init()?; + let cache = cache.init()?.with_refresh( + args.refresh + .combine(Refresh::from(args.settings.upgrade.clone())), + ); commands::lock( args.locked, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1534de957cd8..b6ec91a504a0 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -11,7 +11,7 @@ use pypi_types::{Requirement, SupportedEnvironments}; use uv_cache::{CacheArgs, Refresh}; use uv_cli::{ options::{flag, resolver_installer_options, resolver_options}, - ExportArgs, ToolUpgradeArgs, + BuildArgs, ExportArgs, ToolUpgradeArgs, }; use uv_cli::{ AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, @@ -1604,7 +1604,43 @@ impl PipCheckSettings { } } -/// The resolved settings to use for a `pip check` invocation. +/// The resolved settings to use for a `build` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct BuildSettings { + pub(crate) src_dir: Option, + pub(crate) sdist: bool, + pub(crate) wheel: bool, + pub(crate) python: Option, + pub(crate) refresh: Refresh, + pub(crate) settings: ResolverSettings, +} + +impl BuildSettings { + /// Resolve the [`BuildSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: BuildArgs, filesystem: Option) -> Self { + let BuildArgs { + src_dir, + sdist, + wheel, + python, + build, + refresh, + resolver, + } = args; + + Self { + src_dir, + sdist, + wheel, + python, + refresh: Refresh::from(refresh), + settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem), + } + } +} + +/// The resolved settings to use for a `venv` invocation. #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct VenvSettings { diff --git a/crates/uv/tests/build.rs b/crates/uv/tests/build.rs new file mode 100644 index 000000000000..9c5da94c48f0 --- /dev/null +++ b/crates/uv/tests/build.rs @@ -0,0 +1,171 @@ +#![cfg(all(feature = "python", feature = "pypi"))] + +use anyhow::Result; +use assert_fs::prelude::*; +use common::{uv_snapshot, TestContext}; + +mod common; + +#[test] +fn build() -> Result<()> { + let context = TestContext::new("3.12"); + + let project = context.temp_dir.child("project"); + + let pyproject_toml = project.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + project.child("src").child("__init__.py").touch()?; + + // Build the specified path. + uv_snapshot!(context.filters(), context.build().arg("project"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Successfully built project-0.1.0.tar.gz and project-0.1.0-py3-none-any.whl + "###); + + // Build the current working directory. + uv_snapshot!(context.filters(), context.build().current_dir(project.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Successfully built project-0.1.0.tar.gz and project-0.1.0-py3-none-any.whl + "###); + + // Error if there's nothing to build. + uv_snapshot!(context.filters(), context.build(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: [TEMP_DIR]/ does not appear to be a Python project, as neither `pyproject.toml` nor `setup.py` are present in the directory + "###); + + Ok(()) +} + +#[test] +fn sdist() -> Result<()> { + let context = TestContext::new("3.12"); + + let project = context.temp_dir.child("project"); + + let pyproject_toml = project.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + project.child("src").child("__init__.py").touch()?; + + // Build the specified path. + uv_snapshot!(context.filters(), context.build().arg("--sdist").current_dir(&project), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Successfully built project-0.1.0.tar.gz + "###); + + Ok(()) +} + +#[test] +fn wheel() -> Result<()> { + let context = TestContext::new("3.12"); + + let project = context.temp_dir.child("project"); + + let pyproject_toml = project.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + project.child("src").child("__init__.py").touch()?; + + // Build the specified path. + uv_snapshot!(context.filters(), context.build().arg("--wheel").current_dir(&project), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Successfully built project-0.1.0-py3-none-any.whl + "###); + + Ok(()) +} + +#[test] +fn sdist_wheel() -> Result<()> { + let context = TestContext::new("3.12"); + + let project = context.temp_dir.child("project"); + + let pyproject_toml = project.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + project.child("src").child("__init__.py").touch()?; + + // Build the specified path. + uv_snapshot!(context.filters(), context.build().arg("--sdist").arg("--wheel").current_dir(&project), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Successfully built project-0.1.0.tar.gz and project-0.1.0-py3-none-any.whl + "###); + + Ok(()) +} diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index d255a7c26296..842590dcd6cb 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -500,6 +500,14 @@ impl TestContext { command } + /// Create a `uv build` command with options shared across scenarios. + pub fn build(&self) -> Command { + let mut command = Command::new(get_bin()); + command.arg("build"); + self.add_shared_args(&mut command); + command + } + /// Create a `uv python find` command with options shared across scenarios. pub fn python_find(&self) -> Command { let mut command = Command::new(get_bin()); diff --git a/crates/uv/tests/help.rs b/crates/uv/tests/help.rs index 87ed0b560871..516408dcdad5 100644 --- a/crates/uv/tests/help.rs +++ b/crates/uv/tests/help.rs @@ -28,6 +28,7 @@ fn help() { python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface venv Create a virtual environment + build Build Python packages into source distributions and wheels cache Manage uv's cache version Display uv's version generate-shell-completion Generate shell completion @@ -92,6 +93,7 @@ fn help_flag() { python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface venv Create a virtual environment + build Build Python packages into source distributions and wheels cache Manage uv's cache version Display uv's version help Display documentation for a command @@ -154,6 +156,7 @@ fn help_short_flag() { python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface venv Create a virtual environment + build Build Python packages into source distributions and wheels cache Manage uv's cache version Display uv's version help Display documentation for a command @@ -633,6 +636,7 @@ fn help_unknown_subcommand() { python pip venv + build cache version generate-shell-completion @@ -657,6 +661,7 @@ fn help_unknown_subcommand() { python pip venv + build cache version generate-shell-completion @@ -708,6 +713,7 @@ fn help_with_global_option() { python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface venv Create a virtual environment + build Build Python packages into source distributions and wheels cache Manage uv's cache version Display uv's version generate-shell-completion Generate shell completion @@ -808,6 +814,7 @@ fn help_with_no_pager() { python Manage Python versions and installations pip Manage Python packages with a pip-compatible interface venv Create a virtual environment + build Build Python packages into source distributions and wheels cache Manage uv's cache version Display uv's version generate-shell-completion Generate shell completion diff --git a/docs/reference/cli.md b/docs/reference/cli.md index f0f0a670cfa9..dc651b995c96 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -36,6 +36,8 @@ uv [OPTIONS]
uv venv

Create a virtual environment

+
uv build

Build Python packages into source distributions and wheels

+
uv cache

Manage uv’s cache

uv version

Display uv’s version

@@ -4477,7 +4479,7 @@ uv pip sync [OPTIONS] ...
--python, -p python

The Python interpreter into which packages should be installed.

-

By default, syncing requires a virtual environment. An path to an alternative Python can be provided, but it is only recommended in continuous integration (CI) environments and should be used with caution, as it can modify the system Python installation.

+

By default, syncing requires a virtual environment. A path to an alternative Python can be provided, but it is only recommended in continuous integration (CI) environments and should be used with caution, as it can modify the system Python installation.

See uv python for details on Python discovery and supported request formats.

@@ -4815,7 +4817,7 @@ uv pip install [OPTIONS] |--editable
--python, -p python

The Python interpreter into which packages should be installed.

-

By default, installation requires a virtual environment. An path to an alternative Python can be provided, but it is only recommended in continuous integration (CI) environments and should be used with caution, as it can modify the system Python installation.

+

By default, installation requires a virtual environment. A path to an alternative Python can be provided, but it is only recommended in continuous integration (CI) environments and should be used with caution, as it can modify the system Python installation.

See uv python for details on Python discovery and supported request formats.

@@ -5037,7 +5039,7 @@ uv pip uninstall [OPTIONS] >
--python, -p python

The Python interpreter from which packages should be uninstalled.

-

By default, uninstallation requires a virtual environment. An path to an alternative Python can be provided, but it is only recommended in continuous integration (CI) environments and should be used with caution, as it can modify the system Python installation.

+

By default, uninstallation requires a virtual environment. A path to an alternative Python can be provided, but it is only recommended in continuous integration (CI) environments and should be used with caution, as it can modify the system Python installation.

See uv python for details on Python discovery and supported request formats.

@@ -5790,6 +5792,246 @@ uv venv [OPTIONS] [NAME]
+## uv build + +Build Python packages into source distributions and wheels. + +By default, `uv build` will build a source distribution ("sdist") from the source directory, and a binary distribution ("wheel") from the source distribution. + +`uv build --sdist` can be used to build only the source distribution, `uv build --wheel` can be used to build only the binary distribution, and `uv build --sdist --wheel` can be used to build both distributions from source. + +

Usage

+ +``` +uv build [OPTIONS] [SRC_DIR] +``` + +

Arguments

+ +
SRC_DIR

The directory from which source distributions and/or wheels should be built.

+ +

Defaults to the current working directory.

+ +
+ +

Options

+ +
--allow-insecure-host allow-insecure-host

Allow insecure connections to a host.

+ +

Can be provided multiple times.

+ +

Expects to receive either a hostname (e.g., localhost), a host-port pair (e.g., localhost:8080), or a URL (e.g., https://localhost).

+ +

WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

+ +
--cache-dir cache-dir

Path to the cache directory.

+ +

Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

+ +
--color color-choice

Control colors in output

+ +

[default: auto]

+

Possible values:

+ +
    +
  • auto: Enables colored output only when the output is going to a terminal or TTY with support
  • + +
  • always: Enables colored output regardless of the detected environment
  • + +
  • never: Disables colored output
  • +
+
--config-file config-file

The path to a uv.toml file to use for configuration.

+ +

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

+ +
--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+ +
--exclude-newer exclude-newer

Limit candidate packages to those that were uploaded prior to the given date.

+ +

Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.

+ +
--extra-index-url extra-index-url

Extra URLs of package indexes to use, in addition to --index-url.

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+ +
--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

+ +

If a path, the target must be a directory that contains packages as wheel files (.whl) or source distributions (.tar.gz or .zip) at the top level.

+ +

If a URL, the page must contain a flat list of links to package files adhering to the formats described above.

+ +
--help, -h

Display the concise help for this command

+ +
--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

+ +

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.

+ +

Possible values:

+ +
    +
  • first-index: Only use results from the first index that returns a match for a given package name
  • + +
  • unsafe-first-match: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next
  • + +
  • unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
  • +
+
--index-url, -i index-url

The URL of the Python package index (by default: <https://pypi.org/simple>).

+ +

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

+ +

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+ +
--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

+ +

At present, only --keyring-provider subprocess is supported, which configures uv to use the keyring CLI to handle authentication.

+ +

Defaults to disabled.

+ +

Possible values:

+ +
    +
  • disabled: Do not use keyring for credential lookup
  • + +
  • subprocess: Use the keyring command for credential lookup
  • +
+
--link-mode link-mode

The method to use when installing packages from the global cache.

+ +

This option is only used when building source distributions.

+ +

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

+ +

Possible values:

+ +
    +
  • clone: Clone (i.e., copy-on-write) packages from the wheel into the site-packages directory
  • + +
  • copy: Copy packages from the wheel into the site-packages directory
  • + +
  • hardlink: Hard link packages from the wheel into the site-packages directory
  • + +
  • symlink: Symbolically link packages from the wheel into the site-packages directory
  • +
+
--native-tls

Whether to load TLS certificates from the platform’s native certificate store.

+ +

By default, uv loads certificates from the bundled webpki-roots crate. The webpki-roots are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).

+ +

However, in some cases, you may want to use the platform’s native certificate store, especially if you’re relying on a corporate trust root (e.g., for a mandatory proxy) that’s included in your system’s certificate store.

+ +
--no-binary

Don’t install pre-built wheels.

+ +

The given packages will be built and installed from source. The resolver will still use pre-built wheels to extract package metadata, if available.

+ +
--no-binary-package no-binary-package

Don’t install pre-built wheels for a specific package

+ +
--no-build

Don’t build source distributions.

+ +

When enabled, resolving will not run arbitrary Python code. The cached wheels of already-built source distributions will be reused, but operations that require building distributions will exit with an error.

+ +
--no-build-isolation

Disable isolation when building source distributions.

+ +

Assumes that build dependencies specified by PEP 518 are already installed.

+ +
--no-build-isolation-package no-build-isolation-package

Disable isolation when building source distributions for a specific package.

+ +

Assumes that the packages’ build dependencies specified by PEP 518 are already installed.

+ +
--no-build-package no-build-package

Don’t build source distributions for a specific package

+ +
--no-cache, -n

Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation

+ +
--no-config

Avoid discovering configuration files (pyproject.toml, uv.toml).

+ +

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

+ +
--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.

+ +

For example, spinners or progress bars.

+ +
--no-python-downloads

Disable automatic downloads of Python.

+ +
--no-sources

Ignore the tool.uv.sources table when resolving dependencies. Used to lock against the standards-compliant, publishable package metadata, as opposed to using any local or Git sources

+ +
--offline

Disable network access.

+ +

When disabled, uv will only use locally cached data and locally available files.

+ +
--prerelease prerelease

The strategy to use when considering pre-release versions.

+ +

By default, uv will accept pre-releases for packages that only publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (if-necessary-or-explicit).

+ +

Possible values:

+ +
    +
  • disallow: Disallow all pre-release versions
  • + +
  • allow: Allow all pre-release versions
  • + +
  • if-necessary: Allow pre-release versions if all versions of a package are pre-release
  • + +
  • explicit: Allow pre-release versions for first-party packages with explicit pre-release markers in their version requirements
  • + +
  • if-necessary-or-explicit: Allow pre-release versions if all versions of a package are pre-release, or if the package has an explicit pre-release marker in its version requirements
  • +
+
--python, -p python

The Python interpreter to use for the build environment.

+ +

By default, builds are executed in isolated virtual environments. The discovered interpreter will be used to create those environments, and will be symlinked or copied in depending on the platform.

+ +

See uv python to view supported request formats.

+ +
--python-preference python-preference

Whether to prefer uv-managed or system Python installations.

+ +

By default, uv prefers using Python versions it manages. However, it will use system Python installations if a uv-managed Python is not installed. This option allows prioritizing or ignoring system Python installations.

+ +

Possible values:

+ +
    +
  • only-managed: Only use managed Python installations; never use system Python installations
  • + +
  • managed: Prefer managed Python installations over system Python installations
  • + +
  • system: Prefer system Python installations over managed Python installations
  • + +
  • only-system: Only use system Python installations; never use managed Python installations
  • +
+
--quiet, -q

Do not print any output

+ +
--refresh

Refresh all cached data

+ +
--refresh-package refresh-package

Refresh cached data for a specific package

+ +
--resolution resolution

The strategy to use when selecting between the different compatible versions for a given package requirement.

+ +

By default, uv will use the latest compatible version of each package (highest).

+ +

Possible values:

+ +
    +
  • highest: Resolve the highest compatible version of each package
  • + +
  • lowest: Resolve the lowest compatible version of each package
  • + +
  • lowest-direct: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies
  • +
+
--sdist

Build a source distribution ("sdist") from the given directory

+ +
--upgrade, -U

Allow package upgrades, ignoring pinned versions in any existing output file. Implies --refresh

+ +
--upgrade-package, -P upgrade-package

Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies --refresh-package

+ +
--verbose, -v

Use verbose output.

+ +

You can configure fine-grained logging using the RUST_LOG environment variable. (<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)

+ +
--version, -V

Display the uv version

+ +
--wheel

Build a built distribution ("wheel") from the given directory

+ +
+ ## uv cache Manage uv's cache