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, Index, 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
227/// Extract the `--index` and `--default-index` values from [`IndexArgs`].
228pub fn indexes_from_args(
229    default_index: Option<&Maybe<Index>>,
230    index: Option<&[Vec<Maybe<Index>>]>,
231) -> Option<Vec<Index>> {
232    let default_index = default_index
233        .cloned()
234        .and_then(Maybe::into_option)
235        .map(|default_index| vec![default_index]);
236    let index = index.map(|index| {
237        index
238            .iter()
239            .flatten()
240            .cloned()
241            .filter_map(Maybe::into_option)
242            .collect()
243    });
244
245    default_index.combine(index)
246}
247
248impl From<ResolverArgs> for PipOptions {
249    fn from(args: ResolverArgs) -> Self {
250        let ResolverArgs {
251            index_args,
252            upgrade,
253            no_upgrade,
254            upgrade_package,
255            upgrade_group,
256            index_strategy,
257            keyring_provider,
258            resolution,
259            prerelease,
260            pre,
261            fork_strategy,
262            config_setting,
263            config_settings_package,
264            no_build_isolation,
265            no_build_isolation_package,
266            build_isolation,
267            exclude_newer,
268            link_mode,
269            no_sources,
270            no_sources_package,
271            exclude_newer_package,
272        } = args;
273
274        if !upgrade_group.is_empty() {
275            eprintln!(
276                "{}{} `{}` is not supported in `uv pip` commands",
277                "error".bold().red(),
278                ":".bold(),
279                "--upgrade-group".green(),
280            );
281            std::process::exit(2);
282        }
283
284        Self {
285            upgrade: flag(upgrade, no_upgrade, "no-upgrade"),
286            upgrade_package: Some(upgrade_package),
287            index_strategy,
288            keyring_provider,
289            resolution,
290            fork_strategy,
291            prerelease: if pre {
292                Some(PrereleaseMode::Allow)
293            } else {
294                prerelease
295            },
296            config_settings: config_setting
297                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
298            config_settings_package: config_settings_package.map(|config_settings| {
299                config_settings
300                    .into_iter()
301                    .collect::<PackageConfigSettings>()
302            }),
303            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
304            no_build_isolation_package: Some(no_build_isolation_package),
305            exclude_newer,
306            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
307            link_mode,
308            no_sources: if no_sources { Some(true) } else { None },
309            no_sources_package: if no_sources_package.is_empty() {
310                None
311            } else {
312                Some(no_sources_package)
313            },
314            ..Self::from(index_args)
315        }
316    }
317}
318
319impl From<InstallerArgs> for PipOptions {
320    fn from(args: InstallerArgs) -> Self {
321        let InstallerArgs {
322            index_args,
323            reinstall,
324            no_reinstall,
325            reinstall_package,
326            index_strategy,
327            keyring_provider,
328            config_setting,
329            config_settings_package,
330            no_build_isolation,
331            build_isolation,
332            exclude_newer,
333            link_mode,
334            compile_bytecode,
335            no_compile_bytecode,
336            no_sources,
337            no_sources_package,
338            exclude_newer_package,
339        } = args;
340
341        Self {
342            reinstall: flag(reinstall, no_reinstall, "reinstall"),
343            reinstall_package: Some(reinstall_package),
344            index_strategy,
345            keyring_provider,
346            config_settings: config_setting
347                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
348            config_settings_package: config_settings_package.map(|config_settings| {
349                config_settings
350                    .into_iter()
351                    .collect::<PackageConfigSettings>()
352            }),
353            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
354            exclude_newer,
355            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
356            link_mode,
357            compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
358            no_sources: if no_sources { Some(true) } else { None },
359            no_sources_package: if no_sources_package.is_empty() {
360                None
361            } else {
362                Some(no_sources_package)
363            },
364            ..Self::from(index_args)
365        }
366    }
367}
368
369impl From<ResolverInstallerArgs> for PipOptions {
370    fn from(args: ResolverInstallerArgs) -> Self {
371        let ResolverInstallerArgs {
372            index_args,
373            upgrade,
374            no_upgrade,
375            upgrade_package,
376            upgrade_group,
377            reinstall,
378            no_reinstall,
379            reinstall_package,
380            index_strategy,
381            keyring_provider,
382            resolution,
383            prerelease,
384            pre,
385            fork_strategy,
386            config_setting,
387            config_settings_package,
388            no_build_isolation,
389            no_build_isolation_package,
390            build_isolation,
391            exclude_newer,
392            link_mode,
393            compile_bytecode,
394            no_compile_bytecode,
395            no_sources,
396            no_sources_package,
397            exclude_newer_package,
398        } = args;
399
400        if !upgrade_group.is_empty() {
401            eprintln!(
402                "{}{} `{}` is not supported in `uv pip` commands",
403                "error".bold().red(),
404                ":".bold(),
405                "--upgrade-group".green(),
406            );
407            std::process::exit(2);
408        }
409
410        Self {
411            upgrade: flag(upgrade, no_upgrade, "upgrade"),
412            upgrade_package: Some(upgrade_package),
413            reinstall: flag(reinstall, no_reinstall, "reinstall"),
414            reinstall_package: Some(reinstall_package),
415            index_strategy,
416            keyring_provider,
417            resolution,
418            prerelease: if pre {
419                Some(PrereleaseMode::Allow)
420            } else {
421                prerelease
422            },
423            fork_strategy,
424            config_settings: config_setting
425                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
426            config_settings_package: config_settings_package.map(|config_settings| {
427                config_settings
428                    .into_iter()
429                    .collect::<PackageConfigSettings>()
430            }),
431            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
432            no_build_isolation_package: Some(no_build_isolation_package),
433            exclude_newer,
434            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
435            link_mode,
436            compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
437            no_sources: if no_sources { Some(true) } else { None },
438            no_sources_package: if no_sources_package.is_empty() {
439                None
440            } else {
441                Some(no_sources_package)
442            },
443            ..Self::from(index_args)
444        }
445    }
446}
447
448impl From<FetchArgs> for PipOptions {
449    fn from(args: FetchArgs) -> Self {
450        let FetchArgs {
451            index_args,
452            index_strategy,
453            keyring_provider,
454            exclude_newer,
455        } = args;
456
457        Self {
458            index_strategy,
459            keyring_provider,
460            exclude_newer,
461            ..Self::from(index_args)
462        }
463    }
464}
465
466impl From<IndexArgs> for PipOptions {
467    fn from(args: IndexArgs) -> Self {
468        let IndexArgs {
469            default_index,
470            index,
471            index_url,
472            extra_index_url,
473            no_index,
474            find_links,
475        } = args;
476
477        Self {
478            index: indexes_from_args(default_index.as_ref(), index.as_deref()),
479            index_url: index_url.and_then(Maybe::into_option),
480            extra_index_url: extra_index_url.map(|extra_index_urls| {
481                extra_index_urls
482                    .into_iter()
483                    .filter_map(Maybe::into_option)
484                    .collect()
485            }),
486            no_index: if no_index { Some(true) } else { None },
487            find_links: find_links.map(|find_links| {
488                find_links
489                    .into_iter()
490                    .filter_map(Maybe::into_option)
491                    .collect()
492            }),
493            ..Self::default()
494        }
495    }
496}
497
498/// Construct the [`ResolverOptions`] from the [`ResolverArgs`] and [`BuildOptionsArgs`].
499pub fn resolver_options(
500    resolver_args: ResolverArgs,
501    build_args: BuildOptionsArgs,
502) -> ResolverOptions {
503    let ResolverArgs {
504        index_args,
505        upgrade,
506        no_upgrade,
507        upgrade_package,
508        upgrade_group,
509        index_strategy,
510        keyring_provider,
511        resolution,
512        prerelease,
513        pre,
514        fork_strategy,
515        config_setting,
516        config_settings_package,
517        no_build_isolation,
518        no_build_isolation_package,
519        build_isolation,
520        exclude_newer,
521        link_mode,
522        no_sources,
523        no_sources_package,
524        exclude_newer_package,
525    } = resolver_args;
526
527    let BuildOptionsArgs {
528        no_build,
529        build,
530        no_build_package,
531        no_binary,
532        binary,
533        no_binary_package,
534    } = build_args;
535
536    ResolverOptions {
537        index: indexes_from_args(
538            index_args.default_index.as_ref(),
539            index_args.index.as_deref(),
540        ),
541        index_url: index_args.index_url.and_then(Maybe::into_option),
542        extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
543            extra_index_url
544                .into_iter()
545                .filter_map(Maybe::into_option)
546                .collect()
547        }),
548        no_index: if index_args.no_index {
549            Some(true)
550        } else {
551            None
552        },
553        find_links: index_args.find_links.map(|find_links| {
554            find_links
555                .into_iter()
556                .filter_map(Maybe::into_option)
557                .collect()
558        }),
559        upgrade: Upgrade::from_args(
560            flag(upgrade, no_upgrade, "no-upgrade"),
561            upgrade_package.into_iter().map(Requirement::from).collect(),
562            upgrade_group,
563        ),
564        index_strategy,
565        keyring_provider,
566        resolution,
567        prerelease: if pre {
568            Some(PrereleaseMode::Allow)
569        } else {
570            prerelease
571        },
572        fork_strategy,
573        dependency_metadata: None,
574        config_settings: config_setting
575            .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
576        config_settings_package: config_settings_package.map(|config_settings| {
577            config_settings
578                .into_iter()
579                .collect::<PackageConfigSettings>()
580        }),
581        build_isolation: BuildIsolation::from_args(
582            flag(no_build_isolation, build_isolation, "build-isolation"),
583            no_build_isolation_package,
584        ),
585        extra_build_dependencies: None,
586        extra_build_variables: None,
587        exclude_newer: ExcludeNewer::from_args(
588            exclude_newer,
589            exclude_newer_package.unwrap_or_default(),
590        ),
591        link_mode,
592        torch_backend: None,
593        no_build: flag(no_build, build, "build"),
594        no_build_package: if no_build_package.is_empty() {
595            None
596        } else {
597            Some(no_build_package)
598        },
599        no_binary: flag(no_binary, binary, "binary"),
600        no_binary_package: if no_binary_package.is_empty() {
601            None
602        } else {
603            Some(no_binary_package)
604        },
605        no_sources: if no_sources { Some(true) } else { None },
606        no_sources_package: if no_sources_package.is_empty() {
607            None
608        } else {
609            Some(no_sources_package)
610        },
611    }
612}
613
614/// Construct the [`ResolverInstallerOptions`] from the [`ResolverInstallerArgs`] and [`BuildOptionsArgs`].
615pub fn resolver_installer_options(
616    resolver_installer_args: ResolverInstallerArgs,
617    build_args: BuildOptionsArgs,
618) -> ResolverInstallerOptions {
619    let index = indexes_from_args(
620        resolver_installer_args.index_args.default_index.as_ref(),
621        resolver_installer_args.index_args.index.as_deref(),
622    );
623    resolver_installer_options_with_indexes(resolver_installer_args, build_args, index)
624}
625
626/// Construct the [`ResolverInstallerOptions`] with a precomputed list of indexes.
627pub fn resolver_installer_options_with_indexes(
628    resolver_installer_args: ResolverInstallerArgs,
629    build_args: BuildOptionsArgs,
630    index: Option<Vec<Index>>,
631) -> ResolverInstallerOptions {
632    let ResolverInstallerArgs {
633        index_args,
634        upgrade,
635        no_upgrade,
636        upgrade_package,
637        upgrade_group,
638        reinstall,
639        no_reinstall,
640        reinstall_package,
641        index_strategy,
642        keyring_provider,
643        resolution,
644        prerelease,
645        pre,
646        fork_strategy,
647        config_setting,
648        config_settings_package,
649        no_build_isolation,
650        no_build_isolation_package,
651        build_isolation,
652        exclude_newer,
653        exclude_newer_package,
654        link_mode,
655        compile_bytecode,
656        no_compile_bytecode,
657        no_sources,
658        no_sources_package,
659    } = resolver_installer_args;
660
661    let BuildOptionsArgs {
662        no_build,
663        build,
664        no_build_package,
665        no_binary,
666        binary,
667        no_binary_package,
668    } = build_args;
669
670    ResolverInstallerOptions {
671        index,
672        index_url: index_args.index_url.and_then(Maybe::into_option),
673        extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
674            extra_index_url
675                .into_iter()
676                .filter_map(Maybe::into_option)
677                .collect()
678        }),
679        no_index: if index_args.no_index {
680            Some(true)
681        } else {
682            None
683        },
684        find_links: index_args.find_links.map(|find_links| {
685            find_links
686                .into_iter()
687                .filter_map(Maybe::into_option)
688                .collect()
689        }),
690        upgrade: Upgrade::from_args(
691            flag(upgrade, no_upgrade, "upgrade"),
692            upgrade_package.into_iter().map(Requirement::from).collect(),
693            upgrade_group,
694        ),
695        reinstall: Reinstall::from_args(
696            flag(reinstall, no_reinstall, "reinstall"),
697            reinstall_package,
698        ),
699        index_strategy,
700        keyring_provider,
701        resolution,
702        prerelease: if pre {
703            Some(PrereleaseMode::Allow)
704        } else {
705            prerelease
706        },
707        fork_strategy,
708        dependency_metadata: None,
709        config_settings: config_setting
710            .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
711        config_settings_package: config_settings_package.map(|config_settings| {
712            config_settings
713                .into_iter()
714                .collect::<PackageConfigSettings>()
715        }),
716        build_isolation: BuildIsolation::from_args(
717            flag(no_build_isolation, build_isolation, "build-isolation"),
718            no_build_isolation_package,
719        ),
720        extra_build_dependencies: None,
721        extra_build_variables: None,
722        exclude_newer,
723        exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
724        link_mode,
725        compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
726        no_build: flag(no_build, build, "build"),
727        no_build_package: if no_build_package.is_empty() {
728            None
729        } else {
730            Some(no_build_package)
731        },
732        no_binary: flag(no_binary, binary, "binary"),
733        no_binary_package: if no_binary_package.is_empty() {
734            None
735        } else {
736            Some(no_binary_package)
737        },
738        no_sources: if no_sources { Some(true) } else { None },
739        no_sources_package: if no_sources_package.is_empty() {
740            None
741        } else {
742            Some(no_sources_package)
743        },
744        torch_backend: None,
745    }
746}