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
17pub 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 #[expect(clippy::exit)]
35 {
36 std::process::exit(2);
37 }
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum FlagSource {
45 Cli,
47 Env(&'static str),
49 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#[derive(Debug, Clone, Copy)]
65pub enum Flag {
66 Disabled,
68 Enabled {
70 source: FlagSource,
71 name: &'static str,
73 },
74}
75
76impl Flag {
77 pub const fn disabled() -> Self {
79 Self::Disabled
80 }
81
82 pub const fn from_cli(name: &'static str) -> Self {
84 Self::Enabled {
85 source: FlagSource::Cli,
86 name,
87 }
88 }
89
90 pub const fn from_config(name: &'static str) -> Self {
92 Self::Enabled {
93 source: FlagSource::Config,
94 name,
95 }
96 }
97
98 pub fn is_enabled(self) -> bool {
100 matches!(self, Self::Enabled { .. })
101 }
102
103 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
118pub 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
138pub 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
175pub 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
227pub 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
498pub 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
614pub 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
626pub 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}