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            #[allow(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        #[allow(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            exclude_newer_package,
220        } = args;
221
222        Self {
223            upgrade: flag(upgrade, no_upgrade, "no-upgrade"),
224            upgrade_package: Some(upgrade_package),
225            index_strategy,
226            keyring_provider,
227            resolution,
228            fork_strategy,
229            prerelease: if pre {
230                Some(PrereleaseMode::Allow)
231            } else {
232                prerelease
233            },
234            config_settings: config_setting
235                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
236            config_settings_package: config_settings_package.map(|config_settings| {
237                config_settings
238                    .into_iter()
239                    .collect::<PackageConfigSettings>()
240            }),
241            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
242            no_build_isolation_package: Some(no_build_isolation_package),
243            exclude_newer,
244            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
245            link_mode,
246            no_sources: if no_sources { Some(true) } else { None },
247            ..Self::from(index_args)
248        }
249    }
250}
251
252impl From<InstallerArgs> for PipOptions {
253    fn from(args: InstallerArgs) -> Self {
254        let InstallerArgs {
255            index_args,
256            reinstall,
257            no_reinstall,
258            reinstall_package,
259            index_strategy,
260            keyring_provider,
261            config_setting,
262            config_settings_package,
263            no_build_isolation,
264            build_isolation,
265            exclude_newer,
266            link_mode,
267            compile_bytecode,
268            no_compile_bytecode,
269            no_sources,
270            exclude_newer_package,
271        } = args;
272
273        Self {
274            reinstall: flag(reinstall, no_reinstall, "reinstall"),
275            reinstall_package: Some(reinstall_package),
276            index_strategy,
277            keyring_provider,
278            config_settings: config_setting
279                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
280            config_settings_package: config_settings_package.map(|config_settings| {
281                config_settings
282                    .into_iter()
283                    .collect::<PackageConfigSettings>()
284            }),
285            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
286            exclude_newer,
287            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
288            link_mode,
289            compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
290            no_sources: if no_sources { Some(true) } else { None },
291            ..Self::from(index_args)
292        }
293    }
294}
295
296impl From<ResolverInstallerArgs> for PipOptions {
297    fn from(args: ResolverInstallerArgs) -> Self {
298        let ResolverInstallerArgs {
299            index_args,
300            upgrade,
301            no_upgrade,
302            upgrade_package,
303            reinstall,
304            no_reinstall,
305            reinstall_package,
306            index_strategy,
307            keyring_provider,
308            resolution,
309            prerelease,
310            pre,
311            fork_strategy,
312            config_setting,
313            config_settings_package,
314            no_build_isolation,
315            no_build_isolation_package,
316            build_isolation,
317            exclude_newer,
318            link_mode,
319            compile_bytecode,
320            no_compile_bytecode,
321            no_sources,
322            exclude_newer_package,
323        } = args;
324
325        Self {
326            upgrade: flag(upgrade, no_upgrade, "upgrade"),
327            upgrade_package: Some(upgrade_package),
328            reinstall: flag(reinstall, no_reinstall, "reinstall"),
329            reinstall_package: Some(reinstall_package),
330            index_strategy,
331            keyring_provider,
332            resolution,
333            prerelease: if pre {
334                Some(PrereleaseMode::Allow)
335            } else {
336                prerelease
337            },
338            fork_strategy,
339            config_settings: config_setting
340                .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
341            config_settings_package: config_settings_package.map(|config_settings| {
342                config_settings
343                    .into_iter()
344                    .collect::<PackageConfigSettings>()
345            }),
346            no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
347            no_build_isolation_package: Some(no_build_isolation_package),
348            exclude_newer,
349            exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
350            link_mode,
351            compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
352            no_sources: if no_sources { Some(true) } else { None },
353            ..Self::from(index_args)
354        }
355    }
356}
357
358impl From<FetchArgs> for PipOptions {
359    fn from(args: FetchArgs) -> Self {
360        let FetchArgs {
361            index_args,
362            index_strategy,
363            keyring_provider,
364            exclude_newer,
365        } = args;
366
367        Self {
368            index_strategy,
369            keyring_provider,
370            exclude_newer,
371            ..Self::from(index_args)
372        }
373    }
374}
375
376impl From<IndexArgs> for PipOptions {
377    fn from(args: IndexArgs) -> Self {
378        let IndexArgs {
379            default_index,
380            index,
381            index_url,
382            extra_index_url,
383            no_index,
384            find_links,
385        } = args;
386
387        Self {
388            index: default_index
389                .and_then(Maybe::into_option)
390                .map(|default_index| vec![default_index])
391                .combine(index.map(|index| {
392                    index
393                        .iter()
394                        .flat_map(std::clone::Clone::clone)
395                        .filter_map(Maybe::into_option)
396                        .collect()
397                })),
398            index_url: index_url.and_then(Maybe::into_option),
399            extra_index_url: extra_index_url.map(|extra_index_urls| {
400                extra_index_urls
401                    .into_iter()
402                    .filter_map(Maybe::into_option)
403                    .collect()
404            }),
405            no_index: if no_index { Some(true) } else { None },
406            find_links: find_links.map(|find_links| {
407                find_links
408                    .into_iter()
409                    .filter_map(Maybe::into_option)
410                    .collect()
411            }),
412            ..Self::default()
413        }
414    }
415}
416
417/// Construct the [`ResolverOptions`] from the [`ResolverArgs`] and [`BuildOptionsArgs`].
418pub fn resolver_options(
419    resolver_args: ResolverArgs,
420    build_args: BuildOptionsArgs,
421) -> ResolverOptions {
422    let ResolverArgs {
423        index_args,
424        upgrade,
425        no_upgrade,
426        upgrade_package,
427        index_strategy,
428        keyring_provider,
429        resolution,
430        prerelease,
431        pre,
432        fork_strategy,
433        config_setting,
434        config_settings_package,
435        no_build_isolation,
436        no_build_isolation_package,
437        build_isolation,
438        exclude_newer,
439        link_mode,
440        no_sources,
441        exclude_newer_package,
442    } = resolver_args;
443
444    let BuildOptionsArgs {
445        no_build,
446        build,
447        no_build_package,
448        no_binary,
449        binary,
450        no_binary_package,
451    } = build_args;
452
453    ResolverOptions {
454        index: index_args
455            .default_index
456            .and_then(Maybe::into_option)
457            .map(|default_index| vec![default_index])
458            .combine(index_args.index.map(|index| {
459                index
460                    .into_iter()
461                    .flat_map(|v| v.clone())
462                    .filter_map(Maybe::into_option)
463                    .collect()
464            })),
465        index_url: index_args.index_url.and_then(Maybe::into_option),
466        extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
467            extra_index_url
468                .into_iter()
469                .filter_map(Maybe::into_option)
470                .collect()
471        }),
472        no_index: if index_args.no_index {
473            Some(true)
474        } else {
475            None
476        },
477        find_links: index_args.find_links.map(|find_links| {
478            find_links
479                .into_iter()
480                .filter_map(Maybe::into_option)
481                .collect()
482        }),
483        upgrade: Upgrade::from_args(
484            flag(upgrade, no_upgrade, "no-upgrade"),
485            upgrade_package.into_iter().map(Requirement::from).collect(),
486        ),
487        index_strategy,
488        keyring_provider,
489        resolution,
490        prerelease: if pre {
491            Some(PrereleaseMode::Allow)
492        } else {
493            prerelease
494        },
495        fork_strategy,
496        dependency_metadata: None,
497        config_settings: config_setting
498            .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
499        config_settings_package: config_settings_package.map(|config_settings| {
500            config_settings
501                .into_iter()
502                .collect::<PackageConfigSettings>()
503        }),
504        build_isolation: BuildIsolation::from_args(
505            flag(no_build_isolation, build_isolation, "build-isolation"),
506            no_build_isolation_package,
507        ),
508        extra_build_dependencies: None,
509        extra_build_variables: None,
510        exclude_newer: ExcludeNewer::from_args(
511            exclude_newer,
512            exclude_newer_package.unwrap_or_default(),
513        ),
514        link_mode,
515        torch_backend: None,
516        no_build: flag(no_build, build, "build"),
517        no_build_package: Some(no_build_package),
518        no_binary: flag(no_binary, binary, "binary"),
519        no_binary_package: Some(no_binary_package),
520        no_sources: if no_sources { Some(true) } else { None },
521    }
522}
523
524/// Construct the [`ResolverInstallerOptions`] from the [`ResolverInstallerArgs`] and [`BuildOptionsArgs`].
525pub fn resolver_installer_options(
526    resolver_installer_args: ResolverInstallerArgs,
527    build_args: BuildOptionsArgs,
528) -> ResolverInstallerOptions {
529    let ResolverInstallerArgs {
530        index_args,
531        upgrade,
532        no_upgrade,
533        upgrade_package,
534        reinstall,
535        no_reinstall,
536        reinstall_package,
537        index_strategy,
538        keyring_provider,
539        resolution,
540        prerelease,
541        pre,
542        fork_strategy,
543        config_setting,
544        config_settings_package,
545        no_build_isolation,
546        no_build_isolation_package,
547        build_isolation,
548        exclude_newer,
549        exclude_newer_package,
550        link_mode,
551        compile_bytecode,
552        no_compile_bytecode,
553        no_sources,
554    } = resolver_installer_args;
555
556    let BuildOptionsArgs {
557        no_build,
558        build,
559        no_build_package,
560        no_binary,
561        binary,
562        no_binary_package,
563    } = build_args;
564
565    let default_index = index_args
566        .default_index
567        .and_then(Maybe::into_option)
568        .map(|default_index| vec![default_index]);
569    let index = index_args.index.map(|index| {
570        index
571            .into_iter()
572            .flat_map(|v| v.clone())
573            .filter_map(Maybe::into_option)
574            .collect()
575    });
576
577    ResolverInstallerOptions {
578        index: default_index.combine(index),
579        index_url: index_args.index_url.and_then(Maybe::into_option),
580        extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
581            extra_index_url
582                .into_iter()
583                .filter_map(Maybe::into_option)
584                .collect()
585        }),
586        no_index: if index_args.no_index {
587            Some(true)
588        } else {
589            None
590        },
591        find_links: index_args.find_links.map(|find_links| {
592            find_links
593                .into_iter()
594                .filter_map(Maybe::into_option)
595                .collect()
596        }),
597        upgrade: Upgrade::from_args(
598            flag(upgrade, no_upgrade, "upgrade"),
599            upgrade_package.into_iter().map(Requirement::from).collect(),
600        ),
601        reinstall: Reinstall::from_args(
602            flag(reinstall, no_reinstall, "reinstall"),
603            reinstall_package,
604        ),
605        index_strategy,
606        keyring_provider,
607        resolution,
608        prerelease: if pre {
609            Some(PrereleaseMode::Allow)
610        } else {
611            prerelease
612        },
613        fork_strategy,
614        dependency_metadata: None,
615        config_settings: config_setting
616            .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
617        config_settings_package: config_settings_package.map(|config_settings| {
618            config_settings
619                .into_iter()
620                .collect::<PackageConfigSettings>()
621        }),
622        build_isolation: BuildIsolation::from_args(
623            flag(no_build_isolation, build_isolation, "build-isolation"),
624            no_build_isolation_package,
625        ),
626        extra_build_dependencies: None,
627        extra_build_variables: None,
628        exclude_newer,
629        exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
630        link_mode,
631        compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
632        no_build: flag(no_build, build, "build"),
633        no_build_package: if no_build_package.is_empty() {
634            None
635        } else {
636            Some(no_build_package)
637        },
638        no_binary: flag(no_binary, binary, "binary"),
639        no_binary_package: if no_binary_package.is_empty() {
640            None
641        } else {
642            Some(no_binary_package)
643        },
644        no_sources: if no_sources { Some(true) } else { None },
645        torch_backend: None,
646    }
647}