Skip to main content

uv_cli/
options.rs

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