use std::fmt;
use anstream::eprintln;
use uv_cache::Refresh;
use uv_configuration::{BuildIsolation, Reinstall, Upgrade};
use uv_distribution_types::{ConfigSettings, PackageConfigSettings, Requirement};
use uv_resolver::{ExcludeNewer, ExcludeNewerPackage, PrereleaseMode};
use uv_settings::{Combine, EnvFlag, PipOptions, ResolverInstallerOptions, ResolverOptions};
use uv_warnings::owo_colors::OwoColorize;
use crate::{
BuildOptionsArgs, FetchArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs,
ResolverInstallerArgs,
};
pub fn flag(yes: bool, no: bool, name: &str) -> Option<bool> {
match (yes, no) {
(true, false) => Some(true),
(false, true) => Some(false),
(false, false) => None,
(..) => {
eprintln!(
"{}{} `{}` and `{}` cannot be used together. \
Boolean flags on different levels are currently not supported \
(https://github.com/clap-rs/clap/issues/6049)",
"error".bold().red(),
":".bold(),
format!("--{name}").green(),
format!("--no-{name}").green(),
);
#[expect(clippy::exit)]
{
std::process::exit(2);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlagSource {
Cli,
Env(&'static str),
Config,
}
impl fmt::Display for FlagSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cli => write!(f, "command-line argument"),
Self::Env(name) => write!(f, "environment variable `{name}`"),
Self::Config => write!(f, "workspace configuration"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Flag {
Disabled,
Enabled {
source: FlagSource,
name: &'static str,
},
}
impl Flag {
pub const fn disabled() -> Self {
Self::Disabled
}
pub const fn from_cli(name: &'static str) -> Self {
Self::Enabled {
source: FlagSource::Cli,
name,
}
}
pub const fn from_config(name: &'static str) -> Self {
Self::Enabled {
source: FlagSource::Config,
name,
}
}
pub fn is_enabled(self) -> bool {
matches!(self, Self::Enabled { .. })
}
pub fn source(self) -> Option<FlagSource> {
match self {
Self::Disabled => None,
Self::Enabled { source, .. } => Some(source),
}
}
pub fn name(self) -> Option<&'static str> {
match self {
Self::Disabled => None,
Self::Enabled { name, .. } => Some(name),
}
}
}
impl From<Flag> for bool {
fn from(flag: Flag) -> Self {
flag.is_enabled()
}
}
pub fn resolve_flag(cli_flag: bool, name: &'static str, env_flag: EnvFlag) -> Flag {
if cli_flag {
Flag::Enabled {
source: FlagSource::Cli,
name,
}
} else if env_flag.value == Some(true) {
Flag::Enabled {
source: FlagSource::Env(env_flag.env_var),
name,
}
} else {
Flag::Disabled
}
}
pub fn check_conflicts(flag_a: Flag, flag_b: Flag) {
if let (
Flag::Enabled {
source: source_a,
name: name_a,
},
Flag::Enabled {
source: source_b,
name: name_b,
},
) = (flag_a, flag_b)
{
let display_a = match source_a {
FlagSource::Cli => format!("`--{name_a}`"),
FlagSource::Env(env) => format!("`{env}` (environment variable)"),
FlagSource::Config => format!("`{name_a}` (workspace configuration)"),
};
let display_b = match source_b {
FlagSource::Cli => format!("`--{name_b}`"),
FlagSource::Env(env) => format!("`{env}` (environment variable)"),
FlagSource::Config => format!("`{name_b}` (workspace configuration)"),
};
eprintln!(
"{}{} the argument {} cannot be used with {}",
"error".bold().red(),
":".bold(),
display_a.green(),
display_b.green(),
);
#[expect(clippy::exit)]
{
std::process::exit(2);
}
}
}
impl From<RefreshArgs> for Refresh {
fn from(value: RefreshArgs) -> Self {
let RefreshArgs {
refresh,
no_refresh,
refresh_package,
} = value;
Self::from_args(flag(refresh, no_refresh, "no-refresh"), refresh_package)
}
}
impl From<ResolverArgs> for PipOptions {
fn from(args: ResolverArgs) -> Self {
let ResolverArgs {
index_args,
upgrade,
no_upgrade,
upgrade_package,
index_strategy,
keyring_provider,
resolution,
prerelease,
pre,
fork_strategy,
config_setting,
config_settings_package,
no_build_isolation,
no_build_isolation_package,
build_isolation,
exclude_newer,
link_mode,
no_sources,
no_sources_package,
exclude_newer_package,
} = args;
Self {
upgrade: flag(upgrade, no_upgrade, "no-upgrade"),
upgrade_package: Some(upgrade_package),
index_strategy,
keyring_provider,
resolution,
fork_strategy,
prerelease: if pre {
Some(PrereleaseMode::Allow)
} else {
prerelease
},
config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
config_settings_package: config_settings_package.map(|config_settings| {
config_settings
.into_iter()
.collect::<PackageConfigSettings>()
}),
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package),
exclude_newer,
exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,
no_sources: if no_sources { Some(true) } else { None },
no_sources_package: Some(no_sources_package),
..Self::from(index_args)
}
}
}
impl From<InstallerArgs> for PipOptions {
fn from(args: InstallerArgs) -> Self {
let InstallerArgs {
index_args,
reinstall,
no_reinstall,
reinstall_package,
index_strategy,
keyring_provider,
config_setting,
config_settings_package,
no_build_isolation,
build_isolation,
exclude_newer,
link_mode,
compile_bytecode,
no_compile_bytecode,
no_sources,
no_sources_package,
exclude_newer_package,
} = args;
Self {
reinstall: flag(reinstall, no_reinstall, "reinstall"),
reinstall_package: Some(reinstall_package),
index_strategy,
keyring_provider,
config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
config_settings_package: config_settings_package.map(|config_settings| {
config_settings
.into_iter()
.collect::<PackageConfigSettings>()
}),
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
exclude_newer,
exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
no_sources: if no_sources { Some(true) } else { None },
no_sources_package: Some(no_sources_package),
..Self::from(index_args)
}
}
}
impl From<ResolverInstallerArgs> for PipOptions {
fn from(args: ResolverInstallerArgs) -> Self {
let ResolverInstallerArgs {
index_args,
upgrade,
no_upgrade,
upgrade_package,
reinstall,
no_reinstall,
reinstall_package,
index_strategy,
keyring_provider,
resolution,
prerelease,
pre,
fork_strategy,
config_setting,
config_settings_package,
no_build_isolation,
no_build_isolation_package,
build_isolation,
exclude_newer,
link_mode,
compile_bytecode,
no_compile_bytecode,
no_sources,
no_sources_package,
exclude_newer_package,
} = args;
Self {
upgrade: flag(upgrade, no_upgrade, "upgrade"),
upgrade_package: Some(upgrade_package),
reinstall: flag(reinstall, no_reinstall, "reinstall"),
reinstall_package: Some(reinstall_package),
index_strategy,
keyring_provider,
resolution,
prerelease: if pre {
Some(PrereleaseMode::Allow)
} else {
prerelease
},
fork_strategy,
config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
config_settings_package: config_settings_package.map(|config_settings| {
config_settings
.into_iter()
.collect::<PackageConfigSettings>()
}),
no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package),
exclude_newer,
exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
no_sources: if no_sources { Some(true) } else { None },
no_sources_package: Some(no_sources_package),
..Self::from(index_args)
}
}
}
impl From<FetchArgs> for PipOptions {
fn from(args: FetchArgs) -> Self {
let FetchArgs {
index_args,
index_strategy,
keyring_provider,
exclude_newer,
} = args;
Self {
index_strategy,
keyring_provider,
exclude_newer,
..Self::from(index_args)
}
}
}
impl From<IndexArgs> for PipOptions {
fn from(args: IndexArgs) -> Self {
let IndexArgs {
default_index,
index,
index_url,
extra_index_url,
no_index,
find_links,
} = args;
Self {
index: default_index
.and_then(Maybe::into_option)
.map(|default_index| vec![default_index])
.combine(index.map(|index| {
index
.iter()
.flat_map(std::clone::Clone::clone)
.filter_map(Maybe::into_option)
.collect()
})),
index_url: index_url.and_then(Maybe::into_option),
extra_index_url: extra_index_url.map(|extra_index_urls| {
extra_index_urls
.into_iter()
.filter_map(Maybe::into_option)
.collect()
}),
no_index: if no_index { Some(true) } else { None },
find_links: find_links.map(|find_links| {
find_links
.into_iter()
.filter_map(Maybe::into_option)
.collect()
}),
..Self::default()
}
}
}
pub fn resolver_options(
resolver_args: ResolverArgs,
build_args: BuildOptionsArgs,
) -> ResolverOptions {
let ResolverArgs {
index_args,
upgrade,
no_upgrade,
upgrade_package,
index_strategy,
keyring_provider,
resolution,
prerelease,
pre,
fork_strategy,
config_setting,
config_settings_package,
no_build_isolation,
no_build_isolation_package,
build_isolation,
exclude_newer,
link_mode,
no_sources,
no_sources_package,
exclude_newer_package,
} = resolver_args;
let BuildOptionsArgs {
no_build,
build,
no_build_package,
no_binary,
binary,
no_binary_package,
} = build_args;
ResolverOptions {
index: index_args
.default_index
.and_then(Maybe::into_option)
.map(|default_index| vec![default_index])
.combine(index_args.index.map(|index| {
index
.into_iter()
.flat_map(|v| v.clone())
.filter_map(Maybe::into_option)
.collect()
})),
index_url: index_args.index_url.and_then(Maybe::into_option),
extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
extra_index_url
.into_iter()
.filter_map(Maybe::into_option)
.collect()
}),
no_index: if index_args.no_index {
Some(true)
} else {
None
},
find_links: index_args.find_links.map(|find_links| {
find_links
.into_iter()
.filter_map(Maybe::into_option)
.collect()
}),
upgrade: Upgrade::from_args(
flag(upgrade, no_upgrade, "no-upgrade"),
upgrade_package.into_iter().map(Requirement::from).collect(),
),
index_strategy,
keyring_provider,
resolution,
prerelease: if pre {
Some(PrereleaseMode::Allow)
} else {
prerelease
},
fork_strategy,
dependency_metadata: None,
config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
config_settings_package: config_settings_package.map(|config_settings| {
config_settings
.into_iter()
.collect::<PackageConfigSettings>()
}),
build_isolation: BuildIsolation::from_args(
flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package,
),
extra_build_dependencies: None,
extra_build_variables: None,
exclude_newer: ExcludeNewer::from_args(
exclude_newer,
exclude_newer_package.unwrap_or_default(),
),
link_mode,
torch_backend: None,
no_build: flag(no_build, build, "build"),
no_build_package: Some(no_build_package),
no_binary: flag(no_binary, binary, "binary"),
no_binary_package: Some(no_binary_package),
no_sources: if no_sources { Some(true) } else { None },
no_sources_package: Some(no_sources_package),
}
}
pub fn resolver_installer_options(
resolver_installer_args: ResolverInstallerArgs,
build_args: BuildOptionsArgs,
) -> ResolverInstallerOptions {
let ResolverInstallerArgs {
index_args,
upgrade,
no_upgrade,
upgrade_package,
reinstall,
no_reinstall,
reinstall_package,
index_strategy,
keyring_provider,
resolution,
prerelease,
pre,
fork_strategy,
config_setting,
config_settings_package,
no_build_isolation,
no_build_isolation_package,
build_isolation,
exclude_newer,
exclude_newer_package,
link_mode,
compile_bytecode,
no_compile_bytecode,
no_sources,
no_sources_package,
} = resolver_installer_args;
let BuildOptionsArgs {
no_build,
build,
no_build_package,
no_binary,
binary,
no_binary_package,
} = build_args;
let default_index = index_args
.default_index
.and_then(Maybe::into_option)
.map(|default_index| vec![default_index]);
let index = index_args.index.map(|index| {
index
.into_iter()
.flat_map(|v| v.clone())
.filter_map(Maybe::into_option)
.collect()
});
ResolverInstallerOptions {
index: default_index.combine(index),
index_url: index_args.index_url.and_then(Maybe::into_option),
extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
extra_index_url
.into_iter()
.filter_map(Maybe::into_option)
.collect()
}),
no_index: if index_args.no_index {
Some(true)
} else {
None
},
find_links: index_args.find_links.map(|find_links| {
find_links
.into_iter()
.filter_map(Maybe::into_option)
.collect()
}),
upgrade: Upgrade::from_args(
flag(upgrade, no_upgrade, "upgrade"),
upgrade_package.into_iter().map(Requirement::from).collect(),
),
reinstall: Reinstall::from_args(
flag(reinstall, no_reinstall, "reinstall"),
reinstall_package,
),
index_strategy,
keyring_provider,
resolution,
prerelease: if pre {
Some(PrereleaseMode::Allow)
} else {
prerelease
},
fork_strategy,
dependency_metadata: None,
config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
config_settings_package: config_settings_package.map(|config_settings| {
config_settings
.into_iter()
.collect::<PackageConfigSettings>()
}),
build_isolation: BuildIsolation::from_args(
flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package,
),
extra_build_dependencies: None,
extra_build_variables: None,
exclude_newer,
exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
no_build: flag(no_build, build, "build"),
no_build_package: if no_build_package.is_empty() {
None
} else {
Some(no_build_package)
},
no_binary: flag(no_binary, binary, "binary"),
no_binary_package: if no_binary_package.is_empty() {
None
} else {
Some(no_binary_package)
},
no_sources: if no_sources { Some(true) } else { None },
no_sources_package: if no_sources_package.is_empty() {
None
} else {
Some(no_sources_package)
},
torch_backend: None,
}
}