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: Some(no_sources_package),
297            ..Self::from(index_args)
298        }
299    }
300}
301
302impl From<InstallerArgs> for PipOptions {
303    fn from(args: InstallerArgs) -> Self {
304        let InstallerArgs {
305            index_args,
306            reinstall,
307            no_reinstall,
308            reinstall_package,
309            index_strategy,
310            keyring_provider,
311            config_setting,
312            config_settings_package,
313            no_build_isolation,
314            build_isolation,
315            exclude_newer,
316            link_mode,
317            compile_bytecode,
318            no_compile_bytecode,
319            no_sources,
320            no_sources_package,
321            exclude_newer_package,
322        } = args;
323
324        Self {
325            reinstall: flag(reinstall, no_reinstall, "reinstall"),
326            reinstall_package: Some(reinstall_package),
327            index_strategy,
328            keyring_provider,
329            config_settings: config_setting
330                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
331            config_settings_package: config_settings_package.map(|config_settings| {
332                config_settings
333                    .into_iter()
334                    .collect::<PackageConfigSettings>()
335            }),
336            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
337            exclude_newer,
338            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
339            link_mode,
340            compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
341            no_sources: if no_sources { Some(true) } else { None },
342            no_sources_package: Some(no_sources_package),
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: Some(no_sources_package),
418            ..Self::from(index_args)
419        }
420    }
421}
422
423impl From<FetchArgs> for PipOptions {
424    fn from(args: FetchArgs) -> Self {
425        let FetchArgs {
426            index_args,
427            index_strategy,
428            keyring_provider,
429            exclude_newer,
430        } = args;
431
432        Self {
433            index_strategy,
434            keyring_provider,
435            exclude_newer,
436            ..Self::from(index_args)
437        }
438    }
439}
440
441impl From<IndexArgs> for PipOptions {
442    fn from(args: IndexArgs) -> Self {
443        let IndexArgs {
444            default_index,
445            index,
446            index_url,
447            extra_index_url,
448            no_index,
449            find_links,
450        } = args;
451
452        Self {
453            index: default_index
454                .and_then(Maybe::into_option)
455                .map(|default_index| vec![default_index])
456                .combine(index.map(|index| {
457                    index
458                        .iter()
459                        .flat_map(std::clone::Clone::clone)
460                        .filter_map(Maybe::into_option)
461                        .collect()
462                })),
463            index_url: index_url.and_then(Maybe::into_option),
464            extra_index_url: extra_index_url.map(|extra_index_urls| {
465                extra_index_urls
466                    .into_iter()
467                    .filter_map(Maybe::into_option)
468                    .collect()
469            }),
470            no_index: if no_index { Some(true) } else { None },
471            find_links: find_links.map(|find_links| {
472                find_links
473                    .into_iter()
474                    .filter_map(Maybe::into_option)
475                    .collect()
476            }),
477            ..Self::default()
478        }
479    }
480}
481
482/// Construct the [`ResolverOptions`] from the [`ResolverArgs`] and [`BuildOptionsArgs`].
483pub fn resolver_options(
484    resolver_args: ResolverArgs,
485    build_args: BuildOptionsArgs,
486) -> ResolverOptions {
487    let ResolverArgs {
488        index_args,
489        upgrade,
490        no_upgrade,
491        upgrade_package,
492        upgrade_group,
493        index_strategy,
494        keyring_provider,
495        resolution,
496        prerelease,
497        pre,
498        fork_strategy,
499        config_setting,
500        config_settings_package,
501        no_build_isolation,
502        no_build_isolation_package,
503        build_isolation,
504        exclude_newer,
505        link_mode,
506        no_sources,
507        no_sources_package,
508        exclude_newer_package,
509    } = resolver_args;
510
511    let BuildOptionsArgs {
512        no_build,
513        build,
514        no_build_package,
515        no_binary,
516        binary,
517        no_binary_package,
518    } = build_args;
519
520    ResolverOptions {
521        index: index_args
522            .default_index
523            .and_then(Maybe::into_option)
524            .map(|default_index| vec![default_index])
525            .combine(index_args.index.map(|index| {
526                index
527                    .into_iter()
528                    .flat_map(|v| v.clone())
529                    .filter_map(Maybe::into_option)
530                    .collect()
531            })),
532        index_url: index_args.index_url.and_then(Maybe::into_option),
533        extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
534            extra_index_url
535                .into_iter()
536                .filter_map(Maybe::into_option)
537                .collect()
538        }),
539        no_index: if index_args.no_index {
540            Some(true)
541        } else {
542            None
543        },
544        find_links: index_args.find_links.map(|find_links| {
545            find_links
546                .into_iter()
547                .filter_map(Maybe::into_option)
548                .collect()
549        }),
550        upgrade: Upgrade::from_args(
551            flag(upgrade, no_upgrade, "no-upgrade"),
552            upgrade_package.into_iter().map(Requirement::from).collect(),
553            upgrade_group,
554        ),
555        index_strategy,
556        keyring_provider,
557        resolution,
558        prerelease: if pre {
559            Some(PrereleaseMode::Allow)
560        } else {
561            prerelease
562        },
563        fork_strategy,
564        dependency_metadata: None,
565        config_settings: config_setting
566            .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
567        config_settings_package: config_settings_package.map(|config_settings| {
568            config_settings
569                .into_iter()
570                .collect::<PackageConfigSettings>()
571        }),
572        build_isolation: BuildIsolation::from_args(
573            flag(no_build_isolation, build_isolation, "build-isolation"),
574            no_build_isolation_package,
575        ),
576        extra_build_dependencies: None,
577        extra_build_variables: None,
578        exclude_newer: ExcludeNewer::from_args(
579            exclude_newer,
580            exclude_newer_package.unwrap_or_default(),
581        ),
582        link_mode,
583        torch_backend: None,
584        no_build: flag(no_build, build, "build"),
585        no_build_package: Some(no_build_package),
586        no_binary: flag(no_binary, binary, "binary"),
587        no_binary_package: Some(no_binary_package),
588        no_sources: if no_sources { Some(true) } else { None },
589        no_sources_package: Some(no_sources_package),
590    }
591}
592
593/// Construct the [`ResolverInstallerOptions`] from the [`ResolverInstallerArgs`] and [`BuildOptionsArgs`].
594pub fn resolver_installer_options(
595    resolver_installer_args: ResolverInstallerArgs,
596    build_args: BuildOptionsArgs,
597) -> ResolverInstallerOptions {
598    let ResolverInstallerArgs {
599        index_args,
600        upgrade,
601        no_upgrade,
602        upgrade_package,
603        upgrade_group,
604        reinstall,
605        no_reinstall,
606        reinstall_package,
607        index_strategy,
608        keyring_provider,
609        resolution,
610        prerelease,
611        pre,
612        fork_strategy,
613        config_setting,
614        config_settings_package,
615        no_build_isolation,
616        no_build_isolation_package,
617        build_isolation,
618        exclude_newer,
619        exclude_newer_package,
620        link_mode,
621        compile_bytecode,
622        no_compile_bytecode,
623        no_sources,
624        no_sources_package,
625    } = resolver_installer_args;
626
627    let BuildOptionsArgs {
628        no_build,
629        build,
630        no_build_package,
631        no_binary,
632        binary,
633        no_binary_package,
634    } = build_args;
635
636    let default_index = index_args
637        .default_index
638        .and_then(Maybe::into_option)
639        .map(|default_index| vec![default_index]);
640    let index = index_args.index.map(|index| {
641        index
642            .into_iter()
643            .flat_map(|v| v.clone())
644            .filter_map(Maybe::into_option)
645            .collect()
646    });
647
648    ResolverInstallerOptions {
649        index: default_index.combine(index),
650        index_url: index_args.index_url.and_then(Maybe::into_option),
651        extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
652            extra_index_url
653                .into_iter()
654                .filter_map(Maybe::into_option)
655                .collect()
656        }),
657        no_index: if index_args.no_index {
658            Some(true)
659        } else {
660            None
661        },
662        find_links: index_args.find_links.map(|find_links| {
663            find_links
664                .into_iter()
665                .filter_map(Maybe::into_option)
666                .collect()
667        }),
668        upgrade: Upgrade::from_args(
669            flag(upgrade, no_upgrade, "upgrade"),
670            upgrade_package.into_iter().map(Requirement::from).collect(),
671            upgrade_group,
672        ),
673        reinstall: Reinstall::from_args(
674            flag(reinstall, no_reinstall, "reinstall"),
675            reinstall_package,
676        ),
677        index_strategy,
678        keyring_provider,
679        resolution,
680        prerelease: if pre {
681            Some(PrereleaseMode::Allow)
682        } else {
683            prerelease
684        },
685        fork_strategy,
686        dependency_metadata: None,
687        config_settings: config_setting
688            .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
689        config_settings_package: config_settings_package.map(|config_settings| {
690            config_settings
691                .into_iter()
692                .collect::<PackageConfigSettings>()
693        }),
694        build_isolation: BuildIsolation::from_args(
695            flag(no_build_isolation, build_isolation, "build-isolation"),
696            no_build_isolation_package,
697        ),
698        extra_build_dependencies: None,
699        extra_build_variables: None,
700        exclude_newer,
701        exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
702        link_mode,
703        compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
704        no_build: flag(no_build, build, "build"),
705        no_build_package: if no_build_package.is_empty() {
706            None
707        } else {
708            Some(no_build_package)
709        },
710        no_binary: flag(no_binary, binary, "binary"),
711        no_binary_package: if no_binary_package.is_empty() {
712            None
713        } else {
714            Some(no_binary_package)
715        },
716        no_sources: if no_sources { Some(true) } else { None },
717        no_sources_package: if no_sources_package.is_empty() {
718            None
719        } else {
720            Some(no_sources_package)
721        },
722        torch_backend: None,
723    }
724}