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