Skip to main content

uv_cli/
options.rs

1use std::error::Error;
2use std::fmt;
3
4use anyhow::bail;
5
6use uv_cache::Refresh;
7use uv_configuration::{BuildIsolation, Reinstall, Upgrade};
8use uv_distribution_types::{ConfigSettings, Index, PackageConfigSettings, Requirement};
9use uv_resolver::{ExcludeNewerPackage, PrereleaseMode};
10use uv_settings::{Combine, EnvFlag, PipOptions, ResolverInstallerOptions, ResolverOptions};
11use uv_warnings::owo_colors::OwoColorize;
12
13use crate::{
14    BuildOptionsArgs, FetchArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs,
15    ResolverInstallerArgs,
16};
17
18/// An error caused by an invalid combination of command-line arguments.
19#[derive(Debug)]
20pub struct ArgumentError(String);
21
22impl fmt::Display for ArgumentError {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        self.0.fmt(formatter)
25    }
26}
27
28impl Error for ArgumentError {}
29
30/// Given a boolean flag pair (like `--upgrade` and `--no-upgrade`), resolve the value of the flag.
31pub fn flag(yes: bool, no: bool, name: &str) -> anyhow::Result<Option<bool>> {
32    match (yes, no) {
33        (true, false) => Ok(Some(true)),
34        (false, true) => Ok(Some(false)),
35        (false, false) => Ok(None),
36        (..) => {
37            bail!(ArgumentError(format!(
38                "`{}` and `{}` cannot be used together. \
39                Boolean flags on different levels are currently not supported \
40                (https://github.com/clap-rs/clap/issues/6049)",
41                format!("--{name}").green(),
42                format!("--no-{name}").green(),
43            )));
44        }
45    }
46}
47
48/// The source of a boolean flag value.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum FlagSource {
51    /// The flag was set via command-line argument.
52    Cli,
53    /// The flag was set via environment variable.
54    Env(&'static str),
55    /// The flag was set via workspace/project configuration.
56    Config,
57}
58
59impl fmt::Display for FlagSource {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::Cli => write!(f, "command-line argument"),
63            Self::Env(name) => write!(f, "environment variable `{name}`"),
64            Self::Config => write!(f, "workspace configuration"),
65        }
66    }
67}
68
69/// A boolean flag value with its source.
70#[derive(Debug, Clone, Copy)]
71pub enum Flag {
72    /// The flag is not set.
73    Disabled,
74    /// The flag is enabled with a known source.
75    Enabled {
76        source: FlagSource,
77        /// The CLI flag name (e.g., "locked" for `--locked`).
78        name: &'static str,
79    },
80}
81
82impl Flag {
83    /// Create a flag that is explicitly disabled.
84    pub const fn disabled() -> Self {
85        Self::Disabled
86    }
87
88    /// Create an enabled flag from a CLI argument.
89    pub const fn from_cli(name: &'static str) -> Self {
90        Self::Enabled {
91            source: FlagSource::Cli,
92            name,
93        }
94    }
95
96    /// Create an enabled flag from workspace/project configuration.
97    pub const fn from_config(name: &'static str) -> Self {
98        Self::Enabled {
99            source: FlagSource::Config,
100            name,
101        }
102    }
103
104    /// Returns `true` if the flag is set.
105    pub fn is_enabled(self) -> bool {
106        matches!(self, Self::Enabled { .. })
107    }
108
109    /// Returns the source of the flag, if it is set.
110    pub fn source(self) -> Option<FlagSource> {
111        match self {
112            Self::Disabled => None,
113            Self::Enabled { source, .. } => Some(source),
114        }
115    }
116}
117
118impl From<Flag> for bool {
119    fn from(flag: Flag) -> Self {
120        flag.is_enabled()
121    }
122}
123
124/// Resolve a boolean flag from CLI arguments and an environment variable.
125///
126/// The CLI argument takes precedence over the environment variable. Returns a [`Flag`] with the
127/// resolved value and source.
128pub fn resolve_flag(cli_flag: bool, name: &'static str, env_flag: EnvFlag) -> Flag {
129    if cli_flag {
130        Flag::Enabled {
131            source: FlagSource::Cli,
132            name,
133        }
134    } else if env_flag.value == Some(true) {
135        Flag::Enabled {
136            source: FlagSource::Env(env_flag.env_var),
137            name,
138        }
139    } else {
140        Flag::Disabled
141    }
142}
143
144/// Resolve a pair of mutually exclusive boolean flags from the CLI and environment variables.
145///
146/// If either flag is set on the command line, both environment variables are ignored so the CLI
147/// retains precedence over the full pair.
148pub fn resolve_flag_pair(
149    cli_flag: bool,
150    cli_no_flag: bool,
151    name: &'static str,
152    no_name: &'static str,
153    env_flag: Option<EnvFlag>,
154    env_no_flag: Option<EnvFlag>,
155) -> (Flag, Flag) {
156    if cli_flag || cli_no_flag {
157        (
158            if cli_flag {
159                Flag::from_cli(name)
160            } else {
161                Flag::disabled()
162            },
163            if cli_no_flag {
164                Flag::from_cli(no_name)
165            } else {
166                Flag::disabled()
167            },
168        )
169    } else {
170        (
171            env_flag.map_or_else(Flag::disabled, |env_flag| {
172                resolve_flag(false, name, env_flag)
173            }),
174            env_no_flag.map_or_else(Flag::disabled, |env_no_flag| {
175                resolve_flag(false, no_name, env_no_flag)
176            }),
177        )
178    }
179}
180
181/// Check if two flags conflict and return an error if they do.
182///
183/// This function checks if both flags are enabled (truthy) and reports an error if so, including
184/// the source of each flag (CLI or environment variable) in the error message.
185pub fn check_conflicts(flag_a: Flag, flag_b: Flag) -> anyhow::Result<()> {
186    if let (
187        Flag::Enabled {
188            source: source_a,
189            name: name_a,
190        },
191        Flag::Enabled {
192            source: source_b,
193            name: name_b,
194        },
195    ) = (flag_a, flag_b)
196    {
197        let display_a = match source_a {
198            FlagSource::Cli => format!("`--{name_a}`"),
199            FlagSource::Env(env) => format!("`{env}` (environment variable)"),
200            FlagSource::Config => format!("`{name_a}` (workspace configuration)"),
201        };
202        let display_b = match source_b {
203            FlagSource::Cli => format!("`--{name_b}`"),
204            FlagSource::Env(env) => format!("`{env}` (environment variable)"),
205            FlagSource::Config => format!("`{name_b}` (workspace configuration)"),
206        };
207        bail!(ArgumentError(format!(
208            "the argument {} cannot be used with {}",
209            display_a.green(),
210            display_b.green()
211        )));
212    }
213    Ok(())
214}
215
216impl TryFrom<RefreshArgs> for Refresh {
217    type Error = anyhow::Error;
218
219    fn try_from(value: RefreshArgs) -> anyhow::Result<Self> {
220        let RefreshArgs {
221            refresh,
222            no_refresh,
223            refresh_package,
224        } = value;
225
226        Ok(Self::from_args(
227            flag(refresh, no_refresh, "no-refresh")?,
228            refresh_package,
229        ))
230    }
231}
232
233/// Extract the `--index` and `--default-index` values from [`IndexArgs`].
234pub fn indexes_from_args(
235    default_index: Option<&Maybe<Index>>,
236    index: Option<&[Vec<Maybe<Index>>]>,
237) -> Option<Vec<Index>> {
238    let default_index = default_index
239        .cloned()
240        .and_then(Maybe::into_option)
241        .map(|default_index| vec![default_index]);
242    let index = index.map(|index| {
243        index
244            .iter()
245            .flatten()
246            .cloned()
247            .filter_map(Maybe::into_option)
248            .collect()
249    });
250
251    default_index.combine(index)
252}
253
254impl TryFrom<ResolverArgs> for PipOptions {
255    type Error = anyhow::Error;
256
257    fn try_from(args: ResolverArgs) -> anyhow::Result<Self> {
258        let ResolverArgs {
259            index_args,
260            upgrade,
261            no_upgrade,
262            upgrade_package,
263            upgrade_group,
264            index_strategy,
265            keyring_provider,
266            resolution,
267            prerelease,
268            pre,
269            fork_strategy,
270            config_setting,
271            config_settings_package,
272            no_build_isolation,
273            no_build_isolation_package,
274            build_isolation,
275            exclude_newer,
276            link_mode,
277            no_sources,
278            no_sources_package,
279            exclude_newer_package,
280        } = args;
281
282        if !upgrade_group.is_empty() {
283            bail!(ArgumentError(format!(
284                "`{}` is not supported in `uv pip` commands",
285                "--upgrade-group".green()
286            )));
287        }
288
289        Ok(Self {
290            upgrade: flag(upgrade, no_upgrade, "no-upgrade")?,
291            upgrade_package: Some(upgrade_package),
292            index_strategy,
293            keyring_provider,
294            resolution,
295            fork_strategy,
296            prerelease: if pre {
297                Some(PrereleaseMode::Allow)
298            } else {
299                prerelease
300            },
301            config_settings: config_setting
302                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
303            config_settings_package: config_settings_package.map(|config_settings| {
304                config_settings
305                    .into_iter()
306                    .collect::<PackageConfigSettings>()
307            }),
308            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
309            no_build_isolation_package: Some(no_build_isolation_package),
310            exclude_newer,
311            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
312            link_mode,
313            no_sources: if no_sources { Some(true) } else { None },
314            no_sources_package: if no_sources_package.is_empty() {
315                None
316            } else {
317                Some(no_sources_package)
318            },
319            ..Self::from(index_args)
320        })
321    }
322}
323
324impl TryFrom<InstallerArgs> for PipOptions {
325    type Error = anyhow::Error;
326
327    fn try_from(args: InstallerArgs) -> anyhow::Result<Self> {
328        let InstallerArgs {
329            index_args,
330            reinstall,
331            no_reinstall,
332            reinstall_package,
333            index_strategy,
334            keyring_provider,
335            config_setting,
336            config_settings_package,
337            no_build_isolation,
338            build_isolation,
339            exclude_newer,
340            link_mode,
341            compile_bytecode,
342            no_compile_bytecode,
343            no_sources,
344            no_sources_package,
345            exclude_newer_package,
346        } = args;
347
348        Ok(Self {
349            reinstall: flag(reinstall, no_reinstall, "reinstall")?,
350            reinstall_package: Some(reinstall_package),
351            index_strategy,
352            keyring_provider,
353            config_settings: config_setting
354                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
355            config_settings_package: config_settings_package.map(|config_settings| {
356                config_settings
357                    .into_iter()
358                    .collect::<PackageConfigSettings>()
359            }),
360            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
361            exclude_newer,
362            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
363            link_mode,
364            compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
365            no_sources: if no_sources { Some(true) } else { None },
366            no_sources_package: if no_sources_package.is_empty() {
367                None
368            } else {
369                Some(no_sources_package)
370            },
371            ..Self::from(index_args)
372        })
373    }
374}
375
376impl TryFrom<ResolverInstallerArgs> for PipOptions {
377    type Error = anyhow::Error;
378
379    fn try_from(args: ResolverInstallerArgs) -> anyhow::Result<Self> {
380        let ResolverInstallerArgs {
381            index_args,
382            upgrade,
383            no_upgrade,
384            upgrade_package,
385            upgrade_group,
386            reinstall,
387            no_reinstall,
388            reinstall_package,
389            index_strategy,
390            keyring_provider,
391            resolution,
392            prerelease,
393            pre,
394            fork_strategy,
395            config_setting,
396            config_settings_package,
397            no_build_isolation,
398            no_build_isolation_package,
399            build_isolation,
400            exclude_newer,
401            link_mode,
402            compile_bytecode,
403            no_compile_bytecode,
404            no_sources,
405            no_sources_package,
406            exclude_newer_package,
407        } = args;
408
409        if !upgrade_group.is_empty() {
410            bail!(ArgumentError(format!(
411                "`{}` is not supported in `uv pip` commands",
412                "--upgrade-group".green()
413            )));
414        }
415
416        Ok(Self {
417            upgrade: flag(upgrade, no_upgrade, "upgrade")?,
418            upgrade_package: Some(upgrade_package),
419            reinstall: flag(reinstall, no_reinstall, "reinstall")?,
420            reinstall_package: Some(reinstall_package),
421            index_strategy,
422            keyring_provider,
423            resolution,
424            prerelease: if pre {
425                Some(PrereleaseMode::Allow)
426            } else {
427                prerelease
428            },
429            fork_strategy,
430            config_settings: config_setting
431                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
432            config_settings_package: config_settings_package.map(|config_settings| {
433                config_settings
434                    .into_iter()
435                    .collect::<PackageConfigSettings>()
436            }),
437            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
438            no_build_isolation_package: Some(no_build_isolation_package),
439            exclude_newer,
440            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
441            link_mode,
442            compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
443            no_sources: if no_sources { Some(true) } else { None },
444            no_sources_package: if no_sources_package.is_empty() {
445                None
446            } else {
447                Some(no_sources_package)
448            },
449            ..Self::from(index_args)
450        })
451    }
452}
453
454impl From<FetchArgs> for PipOptions {
455    fn from(args: FetchArgs) -> Self {
456        let FetchArgs {
457            index_args,
458            index_strategy,
459            keyring_provider,
460            exclude_newer,
461        } = args;
462
463        Self {
464            index_strategy,
465            keyring_provider,
466            exclude_newer,
467            ..Self::from(index_args)
468        }
469    }
470}
471
472impl From<IndexArgs> for PipOptions {
473    fn from(args: IndexArgs) -> Self {
474        let IndexArgs {
475            default_index,
476            index,
477            index_url,
478            extra_index_url,
479            no_index,
480            find_links,
481        } = args;
482
483        Self {
484            index: indexes_from_args(default_index.as_ref(), index.as_deref()),
485            index_url: index_url.and_then(Maybe::into_option),
486            extra_index_url: extra_index_url.map(|extra_index_urls| {
487                extra_index_urls
488                    .into_iter()
489                    .filter_map(Maybe::into_option)
490                    .collect()
491            }),
492            no_index: if no_index { Some(true) } else { None },
493            find_links: find_links.map(|find_links| {
494                find_links
495                    .into_iter()
496                    .filter_map(Maybe::into_option)
497                    .collect()
498            }),
499            ..Self::default()
500        }
501    }
502}
503
504/// Construct the [`ResolverOptions`] from the [`ResolverArgs`] and [`BuildOptionsArgs`].
505pub fn resolver_options(
506    resolver_args: ResolverArgs,
507    build_args: BuildOptionsArgs,
508) -> anyhow::Result<ResolverOptions> {
509    let ResolverArgs {
510        index_args,
511        upgrade,
512        no_upgrade,
513        upgrade_package,
514        upgrade_group,
515        index_strategy,
516        keyring_provider,
517        resolution,
518        prerelease,
519        pre,
520        fork_strategy,
521        config_setting,
522        config_settings_package,
523        no_build_isolation,
524        no_build_isolation_package,
525        build_isolation,
526        exclude_newer,
527        link_mode,
528        no_sources,
529        no_sources_package,
530        exclude_newer_package,
531    } = resolver_args;
532
533    let BuildOptionsArgs {
534        no_build,
535        build,
536        no_build_package,
537        no_binary,
538        binary,
539        no_binary_package,
540    } = build_args;
541
542    Ok(ResolverOptions {
543        index: indexes_from_args(
544            index_args.default_index.as_ref(),
545            index_args.index.as_deref(),
546        ),
547        index_url: index_args.index_url.and_then(Maybe::into_option),
548        extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
549            extra_index_url
550                .into_iter()
551                .filter_map(Maybe::into_option)
552                .collect()
553        }),
554        no_index: if index_args.no_index {
555            Some(true)
556        } else {
557            None
558        },
559        find_links: index_args.find_links.map(|find_links| {
560            find_links
561                .into_iter()
562                .filter_map(Maybe::into_option)
563                .collect()
564        }),
565        upgrade: Upgrade::from_args(
566            flag(upgrade, no_upgrade, "no-upgrade")?,
567            upgrade_package.into_iter().map(Requirement::from).collect(),
568            upgrade_group,
569        ),
570        index_strategy,
571        keyring_provider,
572        resolution,
573        prerelease: if pre {
574            Some(PrereleaseMode::Allow)
575        } else {
576            prerelease
577        },
578        fork_strategy,
579        dependency_metadata: None,
580        config_settings: config_setting
581            .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
582        config_settings_package: config_settings_package.map(|config_settings| {
583            config_settings
584                .into_iter()
585                .collect::<PackageConfigSettings>()
586        }),
587        build_isolation: BuildIsolation::from_args(
588            flag(no_build_isolation, build_isolation, "build-isolation")?,
589            no_build_isolation_package,
590        ),
591        extra_build_dependencies: None,
592        extra_build_variables: None,
593        exclude_newer,
594        exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
595        link_mode,
596        torch_backend: None,
597        no_build: flag(no_build, build, "build")?,
598        no_build_package: if no_build_package.is_empty() {
599            None
600        } else {
601            Some(no_build_package)
602        },
603        no_binary: flag(no_binary, binary, "binary")?,
604        no_binary_package: if no_binary_package.is_empty() {
605            None
606        } else {
607            Some(no_binary_package)
608        },
609        no_sources: if no_sources { Some(true) } else { None },
610        no_sources_package: if no_sources_package.is_empty() {
611            None
612        } else {
613            Some(no_sources_package)
614        },
615    })
616}
617
618/// Construct the [`ResolverInstallerOptions`] from the [`ResolverInstallerArgs`] and [`BuildOptionsArgs`].
619pub fn resolver_installer_options(
620    resolver_installer_args: ResolverInstallerArgs,
621    build_args: BuildOptionsArgs,
622) -> anyhow::Result<ResolverInstallerOptions> {
623    let index = indexes_from_args(
624        resolver_installer_args.index_args.default_index.as_ref(),
625        resolver_installer_args.index_args.index.as_deref(),
626    );
627    resolver_installer_options_with_indexes(resolver_installer_args, build_args, index)
628}
629
630/// Construct the [`ResolverInstallerOptions`] with a precomputed list of indexes.
631pub fn resolver_installer_options_with_indexes(
632    resolver_installer_args: ResolverInstallerArgs,
633    build_args: BuildOptionsArgs,
634    index: Option<Vec<Index>>,
635) -> anyhow::Result<ResolverInstallerOptions> {
636    let ResolverInstallerArgs {
637        index_args,
638        upgrade,
639        no_upgrade,
640        upgrade_package,
641        upgrade_group,
642        reinstall,
643        no_reinstall,
644        reinstall_package,
645        index_strategy,
646        keyring_provider,
647        resolution,
648        prerelease,
649        pre,
650        fork_strategy,
651        config_setting,
652        config_settings_package,
653        no_build_isolation,
654        no_build_isolation_package,
655        build_isolation,
656        exclude_newer,
657        exclude_newer_package,
658        link_mode,
659        compile_bytecode,
660        no_compile_bytecode,
661        no_sources,
662        no_sources_package,
663    } = resolver_installer_args;
664
665    let BuildOptionsArgs {
666        no_build,
667        build,
668        no_build_package,
669        no_binary,
670        binary,
671        no_binary_package,
672    } = build_args;
673
674    Ok(ResolverInstallerOptions {
675        index,
676        index_url: index_args.index_url.and_then(Maybe::into_option),
677        extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
678            extra_index_url
679                .into_iter()
680                .filter_map(Maybe::into_option)
681                .collect()
682        }),
683        no_index: if index_args.no_index {
684            Some(true)
685        } else {
686            None
687        },
688        find_links: index_args.find_links.map(|find_links| {
689            find_links
690                .into_iter()
691                .filter_map(Maybe::into_option)
692                .collect()
693        }),
694        upgrade: Upgrade::from_args(
695            flag(upgrade, no_upgrade, "upgrade")?,
696            upgrade_package.into_iter().map(Requirement::from).collect(),
697            upgrade_group,
698        ),
699        reinstall: Reinstall::from_args(
700            flag(reinstall, no_reinstall, "reinstall")?,
701            reinstall_package,
702        ),
703        index_strategy,
704        keyring_provider,
705        resolution,
706        prerelease: if pre {
707            Some(PrereleaseMode::Allow)
708        } else {
709            prerelease
710        },
711        fork_strategy,
712        dependency_metadata: None,
713        config_settings: config_setting
714            .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
715        config_settings_package: config_settings_package.map(|config_settings| {
716            config_settings
717                .into_iter()
718                .collect::<PackageConfigSettings>()
719        }),
720        build_isolation: BuildIsolation::from_args(
721            flag(no_build_isolation, build_isolation, "build-isolation")?,
722            no_build_isolation_package,
723        ),
724        extra_build_dependencies: None,
725        extra_build_variables: None,
726        exclude_newer,
727        exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
728        link_mode,
729        compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
730        no_build: flag(no_build, build, "build")?,
731        no_build_package: if no_build_package.is_empty() {
732            None
733        } else {
734            Some(no_build_package)
735        },
736        no_binary: flag(no_binary, binary, "binary")?,
737        no_binary_package: if no_binary_package.is_empty() {
738            None
739        } else {
740            Some(no_binary_package)
741        },
742        no_sources: if no_sources { Some(true) } else { None },
743        no_sources_package: if no_sources_package.is_empty() {
744            None
745        } else {
746            Some(no_sources_package)
747        },
748        torch_backend: None,
749    })
750}