use std::collections::BTreeSet;
use itertools::Itertools;
use owo_colors::OwoColorize;
use tracing::{Level, debug, enabled, warn};
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildIsolation, BuildOptions, Concurrency, Constraints, DryRun, EditableMode,
ExtrasSpecification, HashCheckingMode, IndexStrategy, NoSources, Reinstall, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::{
ConfigSettings, DependencyMetadata, ExtraBuildVariables, Index, IndexLocations,
NameRequirementSpecification, Origin, PackageConfigSettings, Requirement, Resolution,
UnresolvedRequirementSpecification,
};
use uv_fs::Simplified;
use uv_install_wheel::LinkMode;
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_preview::{Preview, PreviewFeature};
use uv_pypi_types::Conflicts;
use uv_python::{
EnvironmentPreference, Prefix, PythonDownloads, PythonEnvironment, PythonInstallation,
PythonPreference, PythonRequest, PythonVersion, Target,
};
use uv_requirements::{GroupsSpecification, RequirementsSource, RequirementsSpecification};
use uv_resolver::{
DependencyMode, ExcludeNewer, FlatIndex, OptionsBuilder, PrereleaseMode, PythonRequirement,
ResolutionMode, ResolverEnvironment,
};
use uv_settings::PythonInstallMirrors;
use uv_torch::{TorchMode, TorchSource, TorchStrategy};
use uv_types::{HashStrategy, SourceTreeEditablePolicy};
use uv_warnings::warn_user;
use uv_workspace::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::editable::apply_editable_mode;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
use crate::commands::pip::operations::Modifications;
use crate::commands::pip::operations::{report_interpreter, report_target_environment};
use crate::commands::pip::{operations, resolution_markers, resolution_tags};
use crate::commands::pylock::{read_pylock_toml, resolve_pylock_toml};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{ExitStatus, diagnostics};
use crate::printer::Printer;
#[expect(clippy::fn_params_excessive_bools)]
pub(crate) async fn pip_install(
requirements: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
excludes: &[RequirementsSource],
build_constraints: &[RequirementsSource],
constraints_from_workspace: Vec<Requirement>,
overrides_from_workspace: Vec<Requirement>,
excludes_from_workspace: Vec<uv_normalize::PackageName>,
build_constraints_from_workspace: Vec<Requirement>,
editable: Option<EditableMode>,
extras: &ExtrasSpecification,
groups: &GroupsSpecification,
resolution_mode: ResolutionMode,
prerelease_mode: PrereleaseMode,
dependency_mode: DependencyMode,
upgrade: Upgrade,
index_locations: IndexLocations,
index_strategy: IndexStrategy,
torch_backend: Option<TorchMode>,
dependency_metadata: DependencyMetadata,
keyring_provider: KeyringProviderType,
client_builder: &BaseClientBuilder<'_>,
reinstall: Reinstall,
link_mode: LinkMode,
compile: bool,
hash_checking: Option<HashCheckingMode>,
installer_metadata: bool,
config_settings: &ConfigSettings,
config_settings_package: &PackageConfigSettings,
build_isolation: BuildIsolation,
extra_build_dependencies: &ExtraBuildDependencies,
extra_build_variables: &ExtraBuildVariables,
build_options: BuildOptions,
modifications: Modifications,
python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>,
python_downloads: PythonDownloads,
install_mirrors: PythonInstallMirrors,
strict: bool,
exclude_newer: ExcludeNewer,
sources: NoSources,
python: Option<String>,
system: bool,
break_system_packages: bool,
target: Option<Target>,
prefix: Option<Prefix>,
python_preference: PythonPreference,
concurrency: Concurrency,
cache: Cache,
workspace_cache: WorkspaceCache,
dry_run: DryRun,
printer: Printer,
preview: Preview,
) -> anyhow::Result<ExitStatus> {
let start = std::time::Instant::now();
let client_builder = client_builder.clone().keyring(keyring_provider);
let RequirementsSpecification {
project,
requirements,
constraints,
overrides,
excludes,
pylock,
source_trees,
groups,
index_url,
extra_index_urls,
no_index,
find_links,
no_binary,
no_build,
extras: _,
} = operations::read_requirements(
requirements,
constraints,
overrides,
excludes,
extras,
Some(groups),
&client_builder,
)
.await?;
if pylock.is_some() {
if !preview.is_enabled(PreviewFeature::Pylock) {
warn_user!(
"The `--pylock` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeature::Pylock
);
}
}
let constraints: Vec<NameRequirementSpecification> = constraints
.iter()
.cloned()
.chain(
constraints_from_workspace
.into_iter()
.map(NameRequirementSpecification::from),
)
.collect();
let overrides: Vec<UnresolvedRequirementSpecification> = overrides
.iter()
.cloned()
.chain(
overrides_from_workspace
.into_iter()
.map(UnresolvedRequirementSpecification::from),
)
.collect();
let excludes: Vec<PackageName> = excludes
.into_iter()
.chain(excludes_from_workspace)
.collect();
let build_constraints: Vec<NameRequirementSpecification> =
operations::read_constraints(build_constraints, &client_builder)
.await?
.into_iter()
.chain(
build_constraints_from_workspace
.iter()
.cloned()
.map(NameRequirementSpecification::from),
)
.collect();
let environment = if target.is_some() || prefix.is_some() {
let python_request = python.as_deref().map(PythonRequest::parse);
let reporter = PythonDownloadReporter::single(printer);
let installation = PythonInstallation::find_or_download(
python_request.as_ref(),
EnvironmentPreference::from_system_flag(system, false),
python_preference.with_system_flag(system),
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?;
report_interpreter(&installation, true, printer)?;
PythonEnvironment::from_installation(installation)
} else {
let environment = PythonEnvironment::find(
&python
.as_deref()
.map(PythonRequest::parse)
.unwrap_or_default(),
EnvironmentPreference::from_system_flag(system, true),
PythonPreference::default().with_system_flag(system),
&cache,
preview,
)?;
report_target_environment(&environment, &cache, printer)?;
environment
};
let extra_build_requires =
LoweredExtraBuildDependencies::from_non_lowered(extra_build_dependencies.clone())
.into_inner();
let environment = if let Some(target) = target {
debug!(
"Using `--target` directory at {}",
target.root().user_display()
);
environment.with_target(target)?
} else if let Some(prefix) = prefix {
debug!(
"Using `--prefix` directory at {}",
prefix.root().user_display()
);
environment.with_prefix(prefix)?
} else {
environment
};
if let Some(externally_managed) = environment.interpreter().is_externally_managed() {
if break_system_packages {
debug!("Ignoring externally managed environment due to `--break-system-packages`");
} else {
let managed_message = match externally_managed.into_error() {
Some(error) => format!(
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n",
environment.root().user_display().cyan(),
textwrap::indent(&error, " ").green(),
),
None => format!(
"The interpreter at {} is externally managed and cannot be modified.",
environment.root().user_display().cyan()
),
};
let error_message = if system {
format!(
"{}\n{}{} Virtual environments were not considered due to the `--system` flag",
managed_message,
"hint".bold().cyan(),
":".bold()
)
} else {
format!(
"{}\n{}{} Consider creating a virtual environment, e.g., with `uv venv`",
managed_message,
"hint".bold().cyan(),
":".bold()
)
};
return Err(anyhow::Error::msg(error_message));
}
}
let _lock = environment
.lock()
.await
.inspect_err(|err| {
warn!("Failed to acquire environment lock: {err}");
})
.ok();
let interpreter = environment.interpreter();
let marker_env = resolution_markers(
python_version.as_ref(),
python_platform.as_ref(),
interpreter,
);
let tags = resolution_tags(
python_version.as_ref(),
python_platform.as_ref(),
interpreter,
)?;
let site_packages = SitePackages::from_environment(&environment)?;
if reinstall.is_none()
&& upgrade.is_none()
&& source_trees.is_empty()
&& groups.is_empty()
&& pylock.is_none()
&& matches!(modifications, Modifications::Sufficient)
{
match site_packages.satisfies_spec(
&requirements,
&constraints,
&overrides,
InstallationStrategy::Permissive,
&marker_env,
&tags,
config_settings,
config_settings_package,
&extra_build_requires,
extra_build_variables,
)? {
SatisfiesResult::Fresh {
recursive_requirements,
} => {
if enabled!(Level::DEBUG) {
for requirement in recursive_requirements
.iter()
.map(ToString::to_string)
.sorted()
{
debug!("Requirement satisfied: {requirement}");
}
}
DefaultInstallLogger.on_check(requirements.len(), start, printer, dry_run)?;
return Ok(ExitStatus::Success);
}
SatisfiesResult::Unsatisfied(requirement) => {
debug!("At least one requirement is not satisfied: {requirement}");
}
}
}
let python_requirement = if let Some(python_version) = python_version.as_ref() {
PythonRequirement::from_python_version(interpreter, python_version)
} else {
PythonRequirement::from_interpreter(interpreter)
};
let hasher = if let Some(hash_checking) = hash_checking {
HashStrategy::from_requirements(
requirements
.iter()
.chain(overrides.iter())
.map(|entry| (&entry.requirement, entry.hashes.as_slice())),
constraints
.iter()
.map(|entry| (&entry.requirement, entry.hashes.as_slice())),
Some(&marker_env),
hash_checking,
)?
} else {
HashStrategy::None
};
let index_locations = index_locations.combine(
extra_index_urls
.into_iter()
.map(Index::from_extra_index_url)
.chain(index_url.map(Index::from_index_url))
.map(|index| index.with_origin(Origin::RequirementsTxt))
.collect(),
find_links
.into_iter()
.map(Index::from_find_links)
.map(|index| index.with_origin(Origin::RequirementsTxt))
.collect(),
no_index,
);
let torch_backend = torch_backend
.map(|mode| {
let source = if uv_auth::PyxTokenStore::from_settings()
.is_ok_and(|store| store.has_credentials())
{
TorchSource::Pyx
} else {
TorchSource::default()
};
TorchStrategy::from_mode(
mode,
source,
python_platform
.map(TargetTriple::platform)
.as_ref()
.unwrap_or(interpreter.platform())
.os(),
)
})
.transpose()?;
let client = RegistryClientBuilder::new(client_builder.clone(), cache.clone())
.index_locations(index_locations.clone())
.index_strategy(index_strategy)
.torch_backend(torch_backend.clone())
.markers(interpreter.markers())
.platform(interpreter.platform())
.build()?;
let build_options = build_options.combine(no_binary, no_build);
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, Some(&tags), &hasher, &build_options)
};
let types_build_isolation = match build_isolation {
BuildIsolation::Isolate => uv_types::BuildIsolation::Isolated,
BuildIsolation::Shared => uv_types::BuildIsolation::Shared(&environment),
BuildIsolation::SharedPackage(ref packages) => {
uv_types::BuildIsolation::SharedPackage(&environment, packages)
}
};
let build_hasher = if hash_checking.is_some() {
HashStrategy::from_requirements(
std::iter::empty(),
build_constraints
.iter()
.map(|entry| (&entry.requirement, entry.hashes.as_slice())),
Some(&marker_env),
HashCheckingMode::Verify,
)?
} else {
HashStrategy::None
};
let build_constraints = Constraints::from_requirements(
build_constraints
.iter()
.map(|constraint| constraint.requirement.clone()),
);
let state = SharedState::default();
let build_dispatch = BuildDispatch::new(
&client,
&cache,
&build_constraints,
interpreter,
&index_locations,
&flat_index,
&dependency_metadata,
state.clone(),
index_strategy,
config_settings,
config_settings_package,
types_build_isolation,
&extra_build_requires,
extra_build_variables,
link_mode,
&build_options,
&build_hasher,
exclude_newer.clone(),
sources.clone(),
SourceTreeEditablePolicy::Project,
workspace_cache.clone(),
concurrency.clone(),
preview,
);
let (resolution, hasher) = if let Some(pylock) = pylock {
let (install_path, lock) = read_pylock_toml(&pylock, &client_builder).await?;
let extras = extras.with_defaults(DefaultExtras::default());
let extras = extras
.extra_names(lock.extras.iter())
.cloned()
.collect::<Vec<_>>();
let groups = groups
.get(&pylock)
.cloned()
.unwrap_or_default()
.with_defaults(DefaultGroups::List(lock.default_groups.clone()));
let groups = groups
.group_names(lock.dependency_groups.iter())
.cloned()
.collect::<Vec<_>>();
resolve_pylock_toml(
lock,
&install_path,
interpreter,
python_version.as_ref(),
python_platform.as_ref(),
&extras,
&groups,
&build_options,
)?
} else {
let preferences = Vec::default();
let options = OptionsBuilder::new()
.resolution_mode(resolution_mode)
.prerelease_mode(prerelease_mode)
.dependency_mode(dependency_mode)
.exclude_newer(exclude_newer.clone())
.index_strategy(index_strategy)
.torch_backend(torch_backend)
.build_options(build_options.clone())
.build();
let (resolution, hasher) = match operations::resolve(
requirements,
constraints,
overrides,
excludes,
source_trees,
project,
BTreeSet::default(),
extras,
&groups,
preferences,
site_packages.clone(),
&hasher,
&reinstall,
&upgrade,
Some(&tags),
ResolverEnvironment::specific(marker_env.clone()),
python_requirement,
interpreter.markers(),
Conflicts::empty(),
&client,
&flat_index,
state.index(),
&build_dispatch,
&concurrency,
options,
Box::new(DefaultResolveLogger),
printer,
)
.await
{
Ok((graph, hasher)) => (Resolution::from(graph), hasher),
Err(err) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
};
(resolution, hasher)
};
let resolution = apply_editable_mode(resolution, editable);
let extra_build_requires = extra_build_requires.match_runtime(&resolution)?;
let build_dispatch = BuildDispatch::new(
&client,
&cache,
&build_constraints,
interpreter,
&index_locations,
&flat_index,
&dependency_metadata,
state.clone(),
index_strategy,
config_settings,
config_settings_package,
types_build_isolation,
&extra_build_requires,
extra_build_variables,
link_mode,
&build_options,
&build_hasher,
exclude_newer.clone(),
sources,
SourceTreeEditablePolicy::Project,
workspace_cache,
concurrency.clone(),
preview,
);
match operations::install(
&resolution,
site_packages,
InstallationStrategy::Permissive,
modifications,
&reinstall,
&build_options,
link_mode,
compile,
&hasher,
&tags,
&client,
state.in_flight(),
&concurrency,
&build_dispatch,
&cache,
&environment,
Box::new(DefaultInstallLogger),
installer_metadata,
dry_run,
printer,
preview,
)
.await
{
Ok(..) => {}
Err(err) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
}
operations::diagnose_resolution(resolution.diagnostics(), printer)?;
if strict && !dry_run.enabled() {
operations::diagnose_environment(
&resolution,
&environment,
&marker_env,
&tags,
&dependency_metadata,
printer,
)?;
}
Ok(ExitStatus::Success)
}