use std::fmt::Write;
use std::str::FromStr;
use anyhow::{Result, bail};
use owo_colors::OwoColorize;
use tracing::{debug, trace};
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DryRun, GitLfsSetting, Reinstall, TargetTriple, Upgrade,
};
use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::{
ExtraBuildRequires, IndexCapabilities, NameRequirementSpecification, Requirement,
RequirementSource, UnresolvedRequirementSpecification,
};
use uv_fs::CWD;
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
use uv_normalize::PackageName;
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
use uv_pep508::MarkerTree;
use uv_preview::Preview;
use uv_python::{
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
PythonPreference, PythonRequest, PythonVersionFile, VersionFileDiscoveryOptions,
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::WorkspaceCache;
use crate::commands::ExitStatus;
use crate::commands::pip::latest::LatestClient;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::{self, Modifications};
use crate::commands::pip::{resolution_markers, resolution_tags};
use crate::commands::project::{
EnvironmentSpecification, PlatformState, ProjectError, resolve_environment, resolve_names,
sync_environment, update_environment,
};
use crate::commands::tool::common::{
finalize_tool_install, refine_interpreter, remove_entrypoints,
};
use crate::commands::tool::{Target, ToolRequest};
use crate::commands::{diagnostics, reporters::PythonDownloadReporter};
use crate::printer::Printer;
use crate::settings::{ResolverInstallerSettings, ResolverSettings};
#[expect(clippy::fn_params_excessive_bools)]
pub(crate) async fn install(
package: String,
editable: bool,
from: Option<String>,
with: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
excludes: &[RequirementsSource],
build_constraints: &[RequirementsSource],
entrypoints: &[PackageName],
lfs: GitLfsSetting,
python: Option<String>,
python_platform: Option<TargetTriple>,
install_mirrors: PythonInstallMirrors,
force: bool,
options: ResolverInstallerOptions,
settings: ResolverInstallerSettings,
client_builder: BaseClientBuilder<'_>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
installer_metadata: bool,
concurrency: Concurrency,
no_config: bool,
cache: Cache,
workspace_cache: &WorkspaceCache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
if settings.resolver.torch_backend.is_some() {
warn_user_once!(
"The `--torch-backend` option is experimental and may change without warning."
);
}
let reporter = PythonDownloadReporter::single(printer);
let (python_request, explicit_python_request) = if let Some(request) = python.as_deref() {
(Some(PythonRequest::parse(request)), true)
} else {
(
PythonVersionFile::discover(
&*CWD,
&VersionFileDiscoveryOptions::default()
.with_no_config(no_config)
.with_no_local(true),
)
.await?
.and_then(PythonVersionFile::into_version),
false,
)
};
let interpreter = PythonInstallation::find_or_download(
python_request.as_ref(),
EnvironmentPreference::OnlySystem,
python_preference,
python_downloads,
&client_builder,
&cache,
Some(&reporter),
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 state = PlatformState::default();
let request = ToolRequest::parse(&package, from.as_deref())?;
let cache = if request.is_latest() {
cache.with_refresh(Refresh::All(Timestamp::now()))
} else {
cache
};
let requirement = match &request {
ToolRequest::Package {
executable,
target: Target::Unspecified(from),
} => {
let source = if editable {
RequirementsSource::from_editable(from)?
} else {
RequirementsSource::from_package(from)?
};
let requirement = RequirementsSpecification::from_source(&source, &client_builder)
.await?
.requirements;
let executable = if let Some(executable) = executable {
let Ok(executable) = PackageName::from_str(executable) else {
bail!(
"Package requirement (`{from}`) provided with `--from` conflicts with install request (`{executable}`)",
from = from.cyan(),
executable = executable.cyan()
)
};
Some(executable)
} else {
None
};
let requirement = resolve_names(
requirement,
&interpreter,
&settings,
&client_builder,
&state,
&concurrency,
&cache,
workspace_cache,
printer,
preview,
lfs,
)
.await?
.pop()
.unwrap();
if let Some(executable) = executable {
if requirement.name != executable {
bail!(
"Package name (`{}`) provided with `--from` does not match install request (`{}`)",
requirement.name.cyan(),
executable.cyan()
);
}
}
requirement
}
ToolRequest::Package {
target: Target::Version(.., name, extras, version),
..
} => {
if editable {
bail!("`--editable` is only supported for local packages");
}
Requirement {
name: name.clone(),
extras: extras.clone(),
groups: Box::new([]),
marker: MarkerTree::default(),
source: RequirementSource::Registry {
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(
version.clone(),
)),
index: None,
conflict: None,
},
origin: None,
}
}
ToolRequest::Package {
target: Target::Latest(.., name, extras),
..
} => {
if editable {
bail!("`--editable` is only supported for local packages");
}
Requirement {
name: name.clone(),
extras: extras.clone(),
groups: Box::new([]),
marker: MarkerTree::default(),
source: RequirementSource::Registry {
specifier: VersionSpecifiers::empty(),
index: None,
conflict: None,
},
origin: None,
}
}
ToolRequest::Python { .. } => {
bail!(
"Cannot install Python with `{}`. Did you mean to use `{}`?",
"uv tool install".cyan(),
"uv python install".cyan(),
);
}
};
let latest = if let ToolRequest::Package {
target: Target::Latest(_, name, _),
..
} = &request
{
let client = RegistryClientBuilder::new(
client_builder
.clone()
.keyring(settings.resolver.keyring_provider),
cache.clone(),
)
.index_locations(settings.resolver.index_locations.clone())
.index_strategy(settings.resolver.index_strategy)
.markers(interpreter.markers())
.platform(interpreter.platform())
.build()?;
let capabilities = IndexCapabilities::default();
let download_concurrency = concurrency.downloads_semaphore.clone();
let latest_client = LatestClient {
client: &client,
capabilities: &capabilities,
prerelease: settings.resolver.prerelease,
exclude_newer: &settings.resolver.exclude_newer,
index_locations: &settings.resolver.index_locations,
tags: None,
requires_python: None,
};
if let Some(dist_filename) = latest_client
.find_latest(name, None, &download_concurrency)
.await?
{
let version = dist_filename.version().clone();
debug!("Resolved `{name}@latest` to `{name}=={version}`");
Some(Requirement {
name: name.clone(),
extras: vec![].into_boxed_slice(),
groups: Box::new([]),
marker: MarkerTree::default(),
source: RequirementSource::Registry {
specifier: VersionSpecifiers::from(VersionSpecifier::equals_version(version)),
index: None,
conflict: None,
},
origin: None,
})
} else {
None
}
} else {
None
};
let package_name = &requirement.name;
let settings = if request.is_latest() {
ResolverInstallerSettings {
resolver: ResolverSettings {
upgrade: Upgrade::package(package_name.clone()).combine(settings.resolver.upgrade),
..settings.resolver
},
..settings
}
} else {
settings
};
let settings = if force {
ResolverInstallerSettings {
reinstall: Reinstall::package(package_name.clone()).combine(settings.reinstall),
..settings
}
} else {
settings
};
let spec = RequirementsSpecification::from_sources(
with,
constraints,
overrides,
excludes,
None,
&client_builder,
)
.await?;
let requirements = {
let mut requirements = Vec::with_capacity(1 + with.len());
requirements.push(requirement.clone());
requirements.extend(
resolve_names(
spec.requirements.clone(),
&interpreter,
&settings,
&client_builder,
&state,
&concurrency,
&cache,
workspace_cache,
printer,
preview,
lfs,
)
.await?,
);
requirements
};
let constraints: Vec<_> = spec
.constraints
.into_iter()
.map(|constraint| constraint.requirement)
.collect();
let overrides = resolve_names(
spec.overrides,
&interpreter,
&settings,
&client_builder,
&state,
&concurrency,
&cache,
workspace_cache,
printer,
preview,
lfs,
)
.await?;
let excludes = spec.excludes.clone();
let build_constraints: Vec<Requirement> =
operations::read_constraints(build_constraints, &client_builder)
.await?
.into_iter()
.map(|constraint| constraint.requirement)
.collect();
let options = ToolOptions::from(options);
let installed_tools = InstalledTools::from_settings()?.init()?;
let _lock = installed_tools.lock().await?;
let (existing_tool_receipt, invalid_tool_receipt) =
match installed_tools.get_tool_receipt(package_name) {
Ok(None) => (None, false),
Ok(Some(receipt)) => (Some(receipt), false),
Err(_) => {
match installed_tools.remove_environment(package_name) {
Ok(()) => {
warn_user!(
"Removed existing `{}` with invalid receipt",
package_name.cyan()
);
}
Err(err)
if err
.as_io_error()
.is_some_and(|err| err.kind() == std::io::ErrorKind::NotFound) => {}
Err(err) => {
return Err(err.into());
}
}
(None, true)
}
};
let existing_environment = if force {
None
} else {
installed_tools
.get_environment(package_name, &cache)?
.filter(|environment| {
existing_environment_usable(
environment.environment(),
&interpreter,
package_name,
explicit_python_request,
&settings,
existing_tool_receipt.as_ref(),
printer,
)
})
};
if let Some(environment) = existing_environment.as_ref().filter(|_| {
!request.is_latest() && settings.reinstall.is_none() && settings.resolver.upgrade.is_none()
}) {
if let Some(tool_receipt) = existing_tool_receipt.as_ref() {
if requirements == tool_receipt.requirements()
&& constraints == tool_receipt.constraints()
&& overrides == tool_receipt.overrides()
&& build_constraints == tool_receipt.build_constraints()
{
let ResolverInstallerSettings {
resolver:
ResolverSettings {
config_setting,
config_settings_package,
extra_build_dependencies,
extra_build_variables,
..
},
..
} = &settings;
let extra_build_requires = LoweredExtraBuildDependencies::from_non_lowered(
extra_build_dependencies.clone(),
)
.into_inner();
let markers = resolution_markers(
None,
python_platform.as_ref(),
environment.environment().interpreter(),
);
let tags = resolution_tags(
None,
python_platform.as_ref(),
environment.environment().interpreter(),
)?;
let site_packages = SitePackages::from_environment(environment.environment())?;
if matches!(
site_packages.satisfies_requirements(
requirements.iter(),
constraints.iter().chain(latest.iter()),
overrides.iter(),
InstallationStrategy::Permissive,
&markers,
&tags,
config_setting,
config_settings_package,
&extra_build_requires,
extra_build_variables,
),
Ok(SatisfiesResult::Fresh { .. })
) {
if *tool_receipt.options() != options {
installed_tools.add_tool_receipt(
package_name,
tool_receipt.clone().with_options(options),
)?;
}
writeln!(
printer.stderr(),
"`{}` is already installed",
requirement.cyan()
)?;
return Ok(ExitStatus::Success);
}
}
}
}
let spec = RequirementsSpecification {
requirements: requirements
.iter()
.cloned()
.map(UnresolvedRequirementSpecification::from)
.collect(),
constraints: constraints
.iter()
.cloned()
.chain(latest.into_iter())
.map(NameRequirementSpecification::from)
.collect(),
overrides: overrides
.iter()
.cloned()
.map(UnresolvedRequirementSpecification::from)
.collect(),
..spec
};
let environment = if let Some(environment) = existing_environment {
let environment = match update_environment(
environment.into_environment(),
spec,
Modifications::Exact,
python_platform.as_ref(),
Constraints::from_requirements(build_constraints.iter().cloned()),
ExtraBuildRequires::default(),
&settings,
&client_builder,
&state,
Box::new(DefaultResolveLogger),
Box::new(DefaultInstallLogger),
installer_metadata,
&concurrency,
&cache,
workspace_cache,
DryRun::Disabled,
printer,
preview,
)
.await
{
Ok(update) => update.into_environment(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
if let Some(existing_receipt) = existing_tool_receipt {
remove_entrypoints(&existing_receipt);
}
environment
} else {
let spec = EnvironmentSpecification::from(spec);
let resolution = resolve_environment(
spec.clone(),
&interpreter,
python_platform.as_ref(),
Constraints::from_requirements(build_constraints.iter().cloned()),
&settings.resolver,
&client_builder,
&state,
Box::new(DefaultResolveLogger),
&concurrency,
&cache,
workspace_cache,
printer,
preview,
)
.await;
let (resolution, interpreter) = match resolution {
Ok(resolution) => (resolution, interpreter),
Err(err) => match err {
ProjectError::Operation(err) => {
let Some(interpreter) = refine_interpreter(
&interpreter,
python_request.as_ref(),
&err,
&client_builder,
&reporter,
&install_mirrors,
python_preference,
python_downloads,
&cache,
preview,
)
.await
.ok()
.flatten() else {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
};
debug!(
"Re-resolving with Python {} (`{}`)",
interpreter.python_version(),
interpreter.sys_executable().display()
);
match resolve_environment(
spec,
&interpreter,
python_platform.as_ref(),
Constraints::from_requirements(build_constraints.iter().cloned()),
&settings.resolver,
&client_builder,
&state,
Box::new(DefaultResolveLogger),
&concurrency,
&cache,
workspace_cache,
printer,
preview,
)
.await
{
Ok(resolution) => (resolution, interpreter),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
}
}
err => return Err(err.into()),
},
};
let environment = installed_tools.create_environment(package_name, interpreter)?;
if let Some(existing_receipt) = existing_tool_receipt {
remove_entrypoints(&existing_receipt);
}
match sync_environment(
environment,
&resolution.into(),
Modifications::Exact,
Constraints::from_requirements(build_constraints.iter().cloned()),
(&settings).into(),
&client_builder,
&state,
Box::new(DefaultInstallLogger),
installer_metadata,
&concurrency,
&cache,
printer,
preview,
)
.await
.inspect_err(|_| {
debug!("Failed to sync environment; removing `{}`", package_name);
let _ = installed_tools.remove_environment(package_name);
}) {
Ok(environment) => environment,
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
}
};
finalize_tool_install(
&environment,
package_name,
entrypoints,
&installed_tools,
&options,
force || invalid_tool_receipt,
if explicit_python_request {
python_request
} else {
None
},
requirements,
constraints,
overrides,
excludes,
build_constraints,
printer,
)?;
Ok(ExitStatus::Success)
}
fn existing_environment_usable(
environment: &PythonEnvironment,
interpreter: &Interpreter,
package_name: &PackageName,
explicit_python_request: bool,
settings: &ResolverInstallerSettings,
existing_tool_receipt: Option<&uv_tool::Tool>,
printer: Printer,
) -> bool {
if environment.uses(interpreter) {
trace!(
"Existing interpreter matches the requested interpreter for `{}`: {}",
package_name,
environment.interpreter().sys_executable().display()
);
return true;
}
if explicit_python_request {
let _ = writeln!(
printer.stderr(),
"Ignoring existing environment for `{}`: the requested Python interpreter does not match the environment interpreter",
package_name.cyan(),
);
return false;
}
if let Some(tool_receipt) = existing_tool_receipt
&& settings.reinstall.contains_package(package_name)
&& tool_receipt.python().is_none()
{
let _ = writeln!(
printer.stderr(),
"Ignoring existing environment for `{from}`: the Python interpreter does not match the environment interpreter",
from = package_name.cyan(),
);
return false;
}
true
}