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