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