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