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