uv/crates/uv/src/commands/pip/install.rs

553 lines
18 KiB
Rust

use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
use std::path::PathBuf;
use anyhow::Context;
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::{
BuildOptions, Concurrency, ConfigSettings, Constraints, DryRun, ExtrasSpecification,
HashCheckingMode, IndexStrategy, PackageConfigSettings, PreviewMode, Reinstall, SourceStrategy,
Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution_types::{
DependencyMetadata, Index, IndexLocations, NameRequirementSpecification, Origin, Requirement,
Resolution, UnresolvedRequirementSpecification,
};
use uv_fs::Simplified;
use uv_install_wheel::LinkMode;
use uv_installer::{SatisfiesResult, SitePackages};
use uv_normalize::GroupName;
use uv_pep508::PackageName;
use uv_pypi_types::Conflicts;
use uv_python::{
EnvironmentPreference, Prefix, PythonEnvironment, PythonInstallation, PythonPreference,
PythonRequest, PythonVersion, Target,
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{
DependencyMode, ExcludeNewer, FlatIndex, OptionsBuilder, PrereleaseMode, PylockToml,
PythonRequirement, ResolutionMode, ResolverEnvironment,
};
use uv_torch::{TorchMode, TorchStrategy};
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user;
use uv_workspace::WorkspaceCache;
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::{ExitStatus, diagnostics};
use crate::printer::Printer;
use crate::settings::NetworkSettings;
/// Install packages into the current environment.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn pip_install(
requirements: &[RequirementsSource],
constraints: &[RequirementsSource],
overrides: &[RequirementsSource],
build_constraints: &[RequirementsSource],
constraints_from_workspace: Vec<Requirement>,
overrides_from_workspace: Vec<Requirement>,
build_constraints_from_workspace: Vec<Requirement>,
extras: &ExtrasSpecification,
groups: BTreeMap<PathBuf, Vec<GroupName>>,
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,
network_settings: &NetworkSettings,
reinstall: Reinstall,
link_mode: LinkMode,
compile: bool,
hash_checking: Option<HashCheckingMode>,
installer_metadata: bool,
config_settings: &ConfigSettings,
config_settings_package: &PackageConfigSettings,
no_build_isolation: bool,
no_build_isolation_package: Vec<PackageName>,
build_options: BuildOptions,
modifications: Modifications,
python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>,
strict: bool,
exclude_newer: Option<ExcludeNewer>,
sources: SourceStrategy,
python: Option<String>,
system: bool,
break_system_packages: bool,
target: Option<Target>,
prefix: Option<Prefix>,
python_preference: PythonPreference,
concurrency: Concurrency,
cache: Cache,
dry_run: DryRun,
printer: Printer,
preview: PreviewMode,
) -> anyhow::Result<ExitStatus> {
let start = std::time::Instant::now();
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.keyring(keyring_provider)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
// Read all requirements from the provided sources.
let RequirementsSpecification {
project,
requirements,
constraints,
overrides,
pylock,
source_trees,
groups,
index_url,
extra_index_urls,
no_index,
find_links,
no_binary,
no_build,
extras: _,
} = operations::read_requirements(
requirements,
constraints,
overrides,
extras,
groups,
&client_builder,
)
.await?;
if pylock.is_some() {
if preview.is_disabled() {
warn_user!(
"The `--pylock` setting is experimental and may change without warning. Pass `--preview` to disable this warning."
);
}
}
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();
// Read build constraints.
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();
// Detect the current Python interpreter.
let environment = if target.is_some() || prefix.is_some() {
let installation = PythonInstallation::find(
&python
.as_deref()
.map(PythonRequest::parse)
.unwrap_or_default(),
EnvironmentPreference::from_system_flag(system, false),
python_preference,
&cache,
preview,
)?;
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),
&cache,
preview,
)?;
report_target_environment(&environment, &cache, printer)?;
environment
};
// Apply any `--target` or `--prefix` directories.
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 the environment is externally managed, abort.
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 {
return if let Some(error) = externally_managed.into_error() {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed, and indicates the following:\n\n{}\n\nConsider creating a virtual environment with `uv venv`.",
environment.root().user_display().cyan(),
textwrap::indent(&error, " ").green(),
))
} else {
Err(anyhow::anyhow!(
"The interpreter at {} is externally managed. Instead, create a virtual environment with `uv venv`.",
environment.root().user_display().cyan()
))
};
}
}
let _lock = environment
.lock()
.await
.inspect_err(|err| {
warn!("Failed to acquire environment lock: {err}");
})
.ok();
// Determine the markers to use for the resolution.
let interpreter = environment.interpreter();
let marker_env = resolution_markers(
python_version.as_ref(),
python_platform.as_ref(),
interpreter,
);
// Determine the set of installed packages.
let site_packages = SitePackages::from_environment(&environment)?;
// Check if the current environment satisfies the requirements.
// Ideally, the resolver would be fast enough to let us remove this check. But right now, for large environments,
// it's an order of magnitude faster to validate the environment than to resolve the requirements.
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, &marker_env)? {
// If the requirements are already satisfied, we're done.
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_audit(requirements.len(), start, printer)?;
if dry_run.enabled() {
writeln!(printer.stderr(), "Would make no changes")?;
}
return Ok(ExitStatus::Success);
}
SatisfiesResult::Unsatisfied(requirement) => {
debug!("At least one requirement is not satisfied: {requirement}");
}
}
}
// Determine the Python requirement, if the user requested a specific version.
let python_requirement = if let Some(python_version) = python_version.as_ref() {
PythonRequirement::from_python_version(interpreter, python_version)
} else {
PythonRequirement::from_interpreter(interpreter)
};
// Determine the tags to use for the resolution.
let tags = resolution_tags(
python_version.as_ref(),
python_platform.as_ref(),
interpreter,
)?;
// Collect the set of required hashes.
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
};
// Incorporate any index locations from the provided sources.
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,
);
index_locations.cache_index_credentials();
// Determine the PyTorch backend.
let torch_backend = torch_backend
.map(|mode| {
TorchStrategy::from_mode(
mode,
python_platform
.map(TargetTriple::platform)
.as_ref()
.unwrap_or(interpreter.platform())
.os(),
)
})
.transpose()?;
// Initialize the registry client.
let client = RegistryClientBuilder::try_from(client_builder)?
.cache(cache.clone())
.index_locations(&index_locations)
.index_strategy(index_strategy)
.torch_backend(torch_backend.clone())
.markers(interpreter.markers())
.platform(interpreter.platform())
.build();
// Combine the `--no-binary` and `--no-build` flags from the requirements files.
let build_options = build_options.combine(no_binary, no_build);
// Resolve the flat indexes from `--find-links`.
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)
};
// Determine whether to enable build isolation.
let build_isolation = if no_build_isolation {
BuildIsolation::Shared(&environment)
} else if no_build_isolation_package.is_empty() {
BuildIsolation::Isolated
} else {
BuildIsolation::SharedPackage(&environment, &no_build_isolation_package)
};
// Enforce (but never require) the build constraints, if `--require-hashes` or `--verify-hashes`
// is provided. _Requiring_ hashes would be too strict, and would break with pip.
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()),
);
// 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,
&dependency_metadata,
state.clone(),
index_strategy,
config_settings,
config_settings_package,
build_isolation,
link_mode,
&build_options,
&build_hasher,
exclude_newer,
sources,
WorkspaceCache::default(),
concurrency,
preview,
);
let (resolution, hasher) = if let Some(pylock) = pylock {
// Read the `pylock.toml` from disk, and deserialize it from TOML.
let install_path = std::path::absolute(&pylock)?;
let install_path = install_path.parent().unwrap();
let content = fs_err::tokio::read_to_string(&pylock).await?;
let lock = toml::from_str::<PylockToml>(&content)
.with_context(|| format!("Not a valid pylock.toml file: {}", pylock.user_display()))?;
let resolution =
lock.to_resolution(install_path, marker_env.markers(), &tags, &build_options)?;
let hasher = HashStrategy::from_resolution(&resolution, HashCheckingMode::Verify)?;
(resolution, hasher)
} else {
// When resolving, don't take any external preferences into account.
let preferences = Vec::default();
let options = OptionsBuilder::new()
.resolution_mode(resolution_mode)
.prerelease_mode(prerelease_mode)
.dependency_mode(dependency_mode)
.exclude_newer(exclude_newer)
.index_strategy(index_strategy)
.torch_backend(torch_backend)
.build_options(build_options.clone())
.build();
// Resolve the requirements.
let resolution = match operations::resolve(
requirements,
constraints,
overrides,
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) => Resolution::from(graph),
Err(err) => {
return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
};
(resolution, hasher)
};
// Sync the environment.
match operations::install(
&resolution,
site_packages,
modifications,
&reinstall,
&build_options,
link_mode,
compile,
&index_locations,
config_settings,
config_settings_package,
&hasher,
&tags,
&client,
state.in_flight(),
concurrency,
&build_dispatch,
&cache,
&environment,
Box::new(DefaultInstallLogger),
installer_metadata,
dry_run,
printer,
)
.await
{
Ok(..) => {}
Err(err) => {
return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
}
// Notify the user of any resolution diagnostics.
operations::diagnose_resolution(resolution.diagnostics(), printer)?;
// Notify the user of any environment diagnostics.
if strict && !dry_run.enabled() {
operations::diagnose_environment(&resolution, &environment, &marker_env, printer)?;
}
Ok(ExitStatus::Success)
}