use std::collections::BTreeSet;
use std::env;
use std::ffi::OsStr;
use std::io::Write;
use std::path::Path;
use std::str::FromStr;
use anyhow::{Result, anyhow};
use itertools::Itertools;
use owo_colors::OwoColorize;
use rustc_hash::FxHashSet;
use tracing::debug;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildIsolation, BuildOptions, Concurrency, Constraints, ExtrasSpecification, IndexStrategy,
NoBinary, NoBuild, NoSources, PipCompileFormat, Reinstall, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::{
ConfigSettings, DependencyMetadata, ExtraBuildVariables, HashGeneration, Index, IndexLocations,
NameRequirementSpecification, Origin, PackageConfigSettings, Requirement, RequiresPython,
UnresolvedRequirementSpecification, Verbatim,
};
use uv_fs::{CWD, Simplified};
use uv_git::ResolvedRepositoryReference;
use uv_install_wheel::LinkMode;
use uv_normalize::PackageName;
use uv_preview::Preview;
use uv_pypi_types::{Conflicts, SupportedEnvironments};
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
PythonPreference, PythonRequest, PythonVersion, VersionRequest,
};
use uv_requirements::upgrade::{LockedRequirements, read_pylock_toml_requirements};
use uv_requirements::{
GroupsSpecification, RequirementsSource, RequirementsSpecification, is_pylock_toml,
upgrade::read_requirements_txt,
};
use uv_resolver::{
AnnotationStyle, DependencyMode, DisplayResolutionGraph, ExcludeNewer, FlatIndex, ForkStrategy,
InMemoryIndex, OptionsBuilder, PrereleaseMode, PylockToml, PythonRequirement, ResolutionMode,
ResolverEnvironment,
};
use uv_settings::PythonInstallMirrors;
use uv_static::EnvVars;
use uv_torch::{TorchMode, TorchSource, TorchStrategy};
use uv_types::{EmptyInstalledPackages, HashStrategy, SourceTreeEditablePolicy};
use uv_warnings::warn_user;
use uv_workspace::WorkspaceCache;
use uv_workspace::pyproject::ExtraBuildDependencies;
use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::{operations, resolution_markers, resolution_tags};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{ExitStatus, OutputWriter, diagnostics};
use crate::printer::Printer;
#[expect(clippy::fn_params_excessive_bools)]
pub(crate) async fn pip_compile(
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>,
environments: SupportedEnvironments,
extras: ExtrasSpecification,
groups: GroupsSpecification,
output_file: Option<&Path>,
format: Option<PipCompileFormat>,
resolution_mode: ResolutionMode,
prerelease_mode: PrereleaseMode,
fork_strategy: ForkStrategy,
dependency_mode: DependencyMode,
upgrade: Upgrade,
generate_hashes: bool,
no_emit_packages: Vec<PackageName>,
include_extras: bool,
include_markers: bool,
include_annotations: bool,
include_header: bool,
custom_compile_command: Option<String>,
include_index_url: bool,
include_find_links: bool,
include_build_options: bool,
include_marker_expression: bool,
include_index_annotation: bool,
index_locations: IndexLocations,
index_strategy: IndexStrategy,
torch_backend: Option<TorchMode>,
dependency_metadata: DependencyMetadata,
keyring_provider: KeyringProviderType,
client_builder: &BaseClientBuilder<'_>,
config_settings: ConfigSettings,
config_settings_package: PackageConfigSettings,
build_isolation: BuildIsolation,
extra_build_dependencies: &ExtraBuildDependencies,
extra_build_variables: &ExtraBuildVariables,
build_options: BuildOptions,
install_mirrors: PythonInstallMirrors,
mut python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>,
python_downloads: PythonDownloads,
universal: bool,
exclude_newer: ExcludeNewer,
sources: NoSources,
annotation_style: AnnotationStyle,
link_mode: LinkMode,
mut python: Option<String>,
system: bool,
python_preference: PythonPreference,
concurrency: Concurrency,
quiet: bool,
cache: Cache,
workspace_cache: WorkspaceCache,
printer: Printer,
preview: Preview,
) -> Result<ExitStatus> {
if output_file
.and_then(Path::file_name)
.is_some_and(|name| name.eq_ignore_ascii_case("pyproject.toml"))
{
return Err(anyhow!(
"`pyproject.toml` is not a supported output format for `{}` (only `requirements.txt`-style output is supported)",
"uv pip compile".green()
));
}
let format = format.unwrap_or_else(|| {
let extension = output_file.and_then(Path::extension);
if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("txt")) {
PipCompileFormat::RequirementsTxt
} else if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) {
PipCompileFormat::PylockToml
} else {
PipCompileFormat::RequirementsTxt
}
});
if matches!(format, PipCompileFormat::PylockToml) {
if let Some(file_name) = output_file
.and_then(Path::file_name)
.and_then(OsStr::to_str)
{
if !is_pylock_toml(file_name) {
return Err(anyhow!(
"Expected the output filename to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`); `{file_name}` won't be recognized as a `pylock.toml` file in subsequent commands",
));
}
}
}
if python.is_none() && python_version.is_none() {
if let Ok(request) = std::env::var(EnvVars::UV_PYTHON) {
if !request.is_empty() {
python = Some(request);
}
}
}
if python_version.is_none() {
if let Some(request) = python.as_ref() {
if let Ok(version) = PythonVersion::from_str(request) {
python_version = Some(version);
python = None;
}
}
}
if !extras.is_empty() && !requirements.iter().any(RequirementsSource::allows_extras) {
return Err(anyhow!(
"Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file."
));
}
let client_builder = client_builder.clone().keyring(keyring_provider);
let RequirementsSpecification {
project,
requirements,
constraints,
overrides,
excludes,
pylock,
source_trees,
groups,
extras: used_extras,
index_url,
extra_index_urls,
no_index,
find_links,
no_binary,
no_build,
} = RequirementsSpecification::from_sources(
requirements,
constraints,
overrides,
excludes,
Some(&groups),
&client_builder,
)
.await?;
if pylock.is_some() {
return Err(anyhow!(
"`pylock.toml` is not a supported input format for `uv pip compile`"
));
}
let constraints = 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
.into_iter()
.map(NameRequirementSpecification::from),
)
.collect();
if source_trees.is_empty() {
let mut unused_extras = extras
.explicit_names()
.filter(|extra| !used_extras.contains(extra))
.collect::<Vec<_>>();
if !unused_extras.is_empty() {
unused_extras.sort_unstable();
unused_extras.dedup();
let s = if unused_extras.len() == 1 { "" } else { "s" };
return Err(anyhow!(
"Requested extra{s} not found: {}",
unused_extras.iter().join(", ")
));
}
}
let environment_preference = EnvironmentPreference::from_system_flag(system, false);
let python_preference = python_preference.with_system_flag(system);
let reporter = PythonDownloadReporter::single(printer);
let interpreter = if let Some(python) = python.as_ref() {
let request = PythonRequest::parse(python);
PythonInstallation::find_or_download(
Some(&request),
environment_preference,
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
} else {
let request = if let Some(version) = python_version.as_ref() {
PythonRequest::Version(VersionRequest::from(version))
} else {
PythonRequest::default()
};
PythonInstallation::find_best(
&request,
environment_preference,
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();
debug!(
"Using Python {} interpreter at {} for builds",
interpreter.python_version(),
interpreter.sys_executable().user_display().cyan()
);
if let Some(python_version) = python_version.as_ref() {
let matches_without_patch = {
python_version.major() == interpreter.python_major()
&& python_version.minor() == interpreter.python_minor()
};
if no_build.is_none()
&& python.is_none()
&& python_version.version() != interpreter.python_version()
&& (python_version.patch().is_some() || !matches_without_patch)
{
warn_user!(
"The requested Python version {} is not available; {} will be used to build dependencies instead.",
python_version.version(),
interpreter.python_version(),
);
}
}
let state = SharedState::default();
let top_level_index = if python_version.is_some() {
InMemoryIndex::default()
} else {
state.index().clone()
};
let python_requirement = if universal {
let requires_python = if let Some(python_version) = python_version.as_ref() {
RequiresPython::greater_than_equal_version(&python_version.version)
} else {
let version = interpreter.python_minor_version();
RequiresPython::greater_than_equal_version(&version)
};
PythonRequirement::from_requires_python(&interpreter, requires_python)
} else if let Some(python_version) = python_version.as_ref() {
PythonRequirement::from_python_version(&interpreter, python_version)
} else {
PythonRequirement::from_interpreter(&interpreter)
};
let artifact_environments = if universal {
environments.clone()
} else {
SupportedEnvironments::default()
};
let (tags, resolver_env) = if universal {
(
None,
ResolverEnvironment::universal(environments.into_markers()),
)
} else {
let tags = resolution_tags(
python_version.as_ref(),
python_platform.as_ref(),
&interpreter,
)?;
let marker_env = resolution_markers(
python_version.as_ref(),
python_platform.as_ref(),
&interpreter,
);
(Some(tags), ResolverEnvironment::specific(marker_env))
};
let hasher = if generate_hashes || matches!(format, PipCompileFormat::PylockToml) {
HashStrategy::Generate(HashGeneration::All)
} 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 LockedRequirements { preferences, git } =
if let Some(output_file) = output_file.filter(|output_file| output_file.exists()) {
match format {
PipCompileFormat::RequirementsTxt => LockedRequirements::from_preferences(
read_requirements_txt(output_file, &upgrade).await?,
),
PipCompileFormat::PylockToml => {
read_pylock_toml_requirements(output_file, &upgrade).await?
}
}
} else {
LockedRequirements::default()
};
for ResolvedRepositoryReference { reference, sha } in git {
debug!("Inserting Git reference into resolver: `{reference:?}` at `{sha}`");
state.git().insert(reference, sha);
}
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, tags.as_deref(), &hasher, &build_options)
};
let environment;
let types_build_isolation = match build_isolation {
BuildIsolation::Isolate => uv_types::BuildIsolation::Isolated,
BuildIsolation::Shared => {
environment = PythonEnvironment::from_interpreter(interpreter.clone());
uv_types::BuildIsolation::Shared(&environment)
}
BuildIsolation::SharedPackage(ref packages) => {
environment = PythonEnvironment::from_interpreter(interpreter.clone());
uv_types::BuildIsolation::SharedPackage(&environment, packages)
}
};
let build_hashes = HashStrategy::None;
let build_constraints = Constraints::from_requirements(
build_constraints
.iter()
.map(|constraint| constraint.requirement.clone()),
);
let extra_build_requires =
LoweredExtraBuildDependencies::from_non_lowered(extra_build_dependencies.clone())
.into_inner();
let build_dispatch = BuildDispatch::new(
&client,
&cache,
&build_constraints,
&interpreter,
&index_locations,
&flat_index,
&dependency_metadata,
state,
index_strategy,
&config_settings,
&config_settings_package,
types_build_isolation,
&extra_build_requires,
extra_build_variables,
link_mode,
&build_options,
&build_hashes,
exclude_newer.clone(),
sources,
SourceTreeEditablePolicy::Project,
workspace_cache,
concurrency.clone(),
preview,
);
let options = OptionsBuilder::new()
.resolution_mode(resolution_mode)
.prerelease_mode(prerelease_mode)
.fork_strategy(fork_strategy)
.dependency_mode(dependency_mode)
.exclude_newer(exclude_newer.clone())
.index_strategy(index_strategy)
.torch_backend(torch_backend)
.build_options(build_options.clone())
.artifact_environments(artifact_environments)
.build();
let resolution = match operations::resolve(
requirements,
constraints,
overrides,
excludes,
source_trees,
project,
BTreeSet::default(),
&extras,
&groups,
preferences,
EmptyInstalledPackages,
&hasher,
&Reinstall::None,
&upgrade,
tags.as_deref(),
resolver_env.clone(),
python_requirement,
interpreter.markers(),
Conflicts::empty(),
&client,
&flat_index,
&top_level_index,
&build_dispatch,
&concurrency,
options,
Box::new(DefaultResolveLogger),
printer,
)
.await
{
Ok((resolution, _)) => resolution,
Err(err) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
};
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file);
if include_header {
writeln!(
writer,
"{}",
"# This file was autogenerated by uv via the following command:".green()
)?;
writeln!(
writer,
"{}",
format!(
"# {}",
cmd(
include_index_url,
include_find_links,
custom_compile_command
)
)
.green()
)?;
}
match format {
PipCompileFormat::RequirementsTxt => {
if include_marker_expression {
if let Some(marker_env) = resolver_env.marker_environment() {
let relevant_markers = resolution.marker_tree(&top_level_index, marker_env)?;
if let Some(relevant_markers) = relevant_markers.contents() {
writeln!(
writer,
"{}",
"# Pinned dependencies known to be valid for:".green()
)?;
writeln!(writer, "{}", format!("# {relevant_markers}").green())?;
}
}
}
let mut wrote_preamble = false;
if include_index_url {
if let Some(index) = index_locations.default_index() {
writeln!(writer, "--index-url {}", index.url().verbatim())?;
wrote_preamble = true;
}
let mut seen = FxHashSet::default();
for extra_index in index_locations.implicit_indexes() {
if seen.insert(extra_index.url()) {
writeln!(writer, "--extra-index-url {}", extra_index.url().verbatim())?;
wrote_preamble = true;
}
}
}
if include_find_links {
for flat_index in index_locations.flat_indexes() {
writeln!(writer, "--find-links {}", flat_index.url().verbatim())?;
wrote_preamble = true;
}
}
if include_build_options {
match build_options.no_binary() {
NoBinary::None => {}
NoBinary::All => {
writeln!(writer, "--no-binary :all:")?;
wrote_preamble = true;
}
NoBinary::Packages(packages) => {
for package in packages {
writeln!(writer, "--no-binary {package}")?;
wrote_preamble = true;
}
}
}
match build_options.no_build() {
NoBuild::None => {}
NoBuild::All => {
writeln!(writer, "--only-binary :all:")?;
wrote_preamble = true;
}
NoBuild::Packages(packages) => {
for package in packages {
writeln!(writer, "--only-binary {package}")?;
wrote_preamble = true;
}
}
}
}
if wrote_preamble {
writeln!(writer)?;
}
write!(
writer,
"{}",
DisplayResolutionGraph::new(
&resolution,
&resolver_env,
&no_emit_packages,
generate_hashes,
include_extras,
include_markers || universal,
include_annotations,
include_index_annotation,
annotation_style,
)
)?;
}
PipCompileFormat::PylockToml => {
if include_marker_expression {
warn_user!(
"The `--emit-marker-expression` option is not supported for `pylock.toml` output"
);
}
if include_index_url {
warn_user!(
"The `--emit-index-url` option is not supported for `pylock.toml` output"
);
}
if include_find_links {
warn_user!(
"The `--emit-find-links` option is not supported for `pylock.toml` output"
);
}
if include_build_options {
warn_user!(
"The `--emit-build-options` option is not supported for `pylock.toml` output"
);
}
if include_index_annotation {
warn_user!(
"The `--emit-index-annotation` option is not supported for `pylock.toml` output"
);
}
let output_file = output_file.map(std::path::absolute).transpose()?;
let install_path = if let Some(output_file) = output_file.as_deref() {
output_file.parent().unwrap()
} else {
&*CWD
};
let export = PylockToml::from_resolution(
&resolution,
&no_emit_packages,
install_path,
tags.as_deref(),
&build_options,
)?;
write!(writer, "{}", export.to_toml()?)?;
}
}
let excluded = no_emit_packages
.into_iter()
.filter(|name| resolution.contains(name))
.collect::<Vec<_>>();
if !excluded.is_empty() {
writeln!(writer)?;
writeln!(
writer,
"{}",
"# The following packages were excluded from the output:".green()
)?;
for package in excluded {
writeln!(writer, "# {package}")?;
}
}
writer.commit().await?;
operations::diagnose_resolution(resolution.diagnostics(), printer)?;
Ok(ExitStatus::Success)
}
fn cmd(
include_index_url: bool,
include_find_links: bool,
custom_compile_command: Option<String>,
) -> String {
if let Some(cmd_str) = custom_compile_command {
return cmd_str;
}
let args = env::args_os()
.skip(1)
.map(|arg| arg.to_string_lossy().to_string())
.scan(None, move |skip_next, arg| {
if matches!(skip_next, Some(true)) {
*skip_next = None;
return Some(None);
}
if !include_index_url {
if arg.starts_with("--extra-index-url=")
|| arg.starts_with("--index-url=")
|| arg.starts_with("-i=")
|| arg.starts_with("--index=")
|| arg.starts_with("--default-index=")
{
*skip_next = None;
return Some(None);
}
if arg == "--index-url"
|| arg == "--extra-index-url"
|| arg == "-i"
|| arg == "--index"
|| arg == "--default-index"
{
*skip_next = Some(true);
return Some(None);
}
}
if !include_find_links {
if arg == "--find-links" || arg == "-f" {
*skip_next = Some(true);
return Some(None);
}
if arg.starts_with("--find-links=") || arg.starts_with("-f") {
*skip_next = None;
return Some(None);
}
}
if arg == "--upgrade" || arg == "-U" {
*skip_next = None;
return Some(None);
}
if arg == "--upgrade-package" || arg == "-P" {
*skip_next = Some(true);
return Some(None);
}
if arg.starts_with("--upgrade-package=") || arg.starts_with("-P") {
*skip_next = None;
return Some(None);
}
if arg == "--quiet" || arg == "-q" {
*skip_next = None;
return Some(None);
}
if arg == "--verbose" || arg == "-v" {
*skip_next = None;
return Some(None);
}
if arg == "--no-progress" {
*skip_next = None;
return Some(None);
}
if arg == "--native-tls" {
*skip_next = None;
return Some(None);
}
Some(Some(arg))
})
.flatten()
.join(" ");
format!("uv {args}")
}