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: Some(no_sources_package),
297 ..Self::from(index_args)
298 }
299 }
300}
301
302impl From<InstallerArgs> for PipOptions {
303 fn from(args: InstallerArgs) -> Self {
304 let InstallerArgs {
305 index_args,
306 reinstall,
307 no_reinstall,
308 reinstall_package,
309 index_strategy,
310 keyring_provider,
311 config_setting,
312 config_settings_package,
313 no_build_isolation,
314 build_isolation,
315 exclude_newer,
316 link_mode,
317 compile_bytecode,
318 no_compile_bytecode,
319 no_sources,
320 no_sources_package,
321 exclude_newer_package,
322 } = args;
323
324 Self {
325 reinstall: flag(reinstall, no_reinstall, "reinstall"),
326 reinstall_package: Some(reinstall_package),
327 index_strategy,
328 keyring_provider,
329 config_settings: config_setting
330 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
331 config_settings_package: config_settings_package.map(|config_settings| {
332 config_settings
333 .into_iter()
334 .collect::<PackageConfigSettings>()
335 }),
336 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
337 exclude_newer,
338 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
339 link_mode,
340 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
341 no_sources: if no_sources { Some(true) } else { None },
342 no_sources_package: Some(no_sources_package),
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: Some(no_sources_package),
418 ..Self::from(index_args)
419 }
420 }
421}
422
423impl From<FetchArgs> for PipOptions {
424 fn from(args: FetchArgs) -> Self {
425 let FetchArgs {
426 index_args,
427 index_strategy,
428 keyring_provider,
429 exclude_newer,
430 } = args;
431
432 Self {
433 index_strategy,
434 keyring_provider,
435 exclude_newer,
436 ..Self::from(index_args)
437 }
438 }
439}
440
441impl From<IndexArgs> for PipOptions {
442 fn from(args: IndexArgs) -> Self {
443 let IndexArgs {
444 default_index,
445 index,
446 index_url,
447 extra_index_url,
448 no_index,
449 find_links,
450 } = args;
451
452 Self {
453 index: default_index
454 .and_then(Maybe::into_option)
455 .map(|default_index| vec![default_index])
456 .combine(index.map(|index| {
457 index
458 .iter()
459 .flat_map(std::clone::Clone::clone)
460 .filter_map(Maybe::into_option)
461 .collect()
462 })),
463 index_url: index_url.and_then(Maybe::into_option),
464 extra_index_url: extra_index_url.map(|extra_index_urls| {
465 extra_index_urls
466 .into_iter()
467 .filter_map(Maybe::into_option)
468 .collect()
469 }),
470 no_index: if no_index { Some(true) } else { None },
471 find_links: find_links.map(|find_links| {
472 find_links
473 .into_iter()
474 .filter_map(Maybe::into_option)
475 .collect()
476 }),
477 ..Self::default()
478 }
479 }
480}
481
482pub fn resolver_options(
484 resolver_args: ResolverArgs,
485 build_args: BuildOptionsArgs,
486) -> ResolverOptions {
487 let ResolverArgs {
488 index_args,
489 upgrade,
490 no_upgrade,
491 upgrade_package,
492 upgrade_group,
493 index_strategy,
494 keyring_provider,
495 resolution,
496 prerelease,
497 pre,
498 fork_strategy,
499 config_setting,
500 config_settings_package,
501 no_build_isolation,
502 no_build_isolation_package,
503 build_isolation,
504 exclude_newer,
505 link_mode,
506 no_sources,
507 no_sources_package,
508 exclude_newer_package,
509 } = resolver_args;
510
511 let BuildOptionsArgs {
512 no_build,
513 build,
514 no_build_package,
515 no_binary,
516 binary,
517 no_binary_package,
518 } = build_args;
519
520 ResolverOptions {
521 index: index_args
522 .default_index
523 .and_then(Maybe::into_option)
524 .map(|default_index| vec![default_index])
525 .combine(index_args.index.map(|index| {
526 index
527 .into_iter()
528 .flat_map(|v| v.clone())
529 .filter_map(Maybe::into_option)
530 .collect()
531 })),
532 index_url: index_args.index_url.and_then(Maybe::into_option),
533 extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
534 extra_index_url
535 .into_iter()
536 .filter_map(Maybe::into_option)
537 .collect()
538 }),
539 no_index: if index_args.no_index {
540 Some(true)
541 } else {
542 None
543 },
544 find_links: index_args.find_links.map(|find_links| {
545 find_links
546 .into_iter()
547 .filter_map(Maybe::into_option)
548 .collect()
549 }),
550 upgrade: Upgrade::from_args(
551 flag(upgrade, no_upgrade, "no-upgrade"),
552 upgrade_package.into_iter().map(Requirement::from).collect(),
553 upgrade_group,
554 ),
555 index_strategy,
556 keyring_provider,
557 resolution,
558 prerelease: if pre {
559 Some(PrereleaseMode::Allow)
560 } else {
561 prerelease
562 },
563 fork_strategy,
564 dependency_metadata: None,
565 config_settings: config_setting
566 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
567 config_settings_package: config_settings_package.map(|config_settings| {
568 config_settings
569 .into_iter()
570 .collect::<PackageConfigSettings>()
571 }),
572 build_isolation: BuildIsolation::from_args(
573 flag(no_build_isolation, build_isolation, "build-isolation"),
574 no_build_isolation_package,
575 ),
576 extra_build_dependencies: None,
577 extra_build_variables: None,
578 exclude_newer: ExcludeNewer::from_args(
579 exclude_newer,
580 exclude_newer_package.unwrap_or_default(),
581 ),
582 link_mode,
583 torch_backend: None,
584 no_build: flag(no_build, build, "build"),
585 no_build_package: Some(no_build_package),
586 no_binary: flag(no_binary, binary, "binary"),
587 no_binary_package: Some(no_binary_package),
588 no_sources: if no_sources { Some(true) } else { None },
589 no_sources_package: Some(no_sources_package),
590 }
591}
592
593pub fn resolver_installer_options(
595 resolver_installer_args: ResolverInstallerArgs,
596 build_args: BuildOptionsArgs,
597) -> ResolverInstallerOptions {
598 let ResolverInstallerArgs {
599 index_args,
600 upgrade,
601 no_upgrade,
602 upgrade_package,
603 upgrade_group,
604 reinstall,
605 no_reinstall,
606 reinstall_package,
607 index_strategy,
608 keyring_provider,
609 resolution,
610 prerelease,
611 pre,
612 fork_strategy,
613 config_setting,
614 config_settings_package,
615 no_build_isolation,
616 no_build_isolation_package,
617 build_isolation,
618 exclude_newer,
619 exclude_newer_package,
620 link_mode,
621 compile_bytecode,
622 no_compile_bytecode,
623 no_sources,
624 no_sources_package,
625 } = resolver_installer_args;
626
627 let BuildOptionsArgs {
628 no_build,
629 build,
630 no_build_package,
631 no_binary,
632 binary,
633 no_binary_package,
634 } = build_args;
635
636 let default_index = index_args
637 .default_index
638 .and_then(Maybe::into_option)
639 .map(|default_index| vec![default_index]);
640 let index = index_args.index.map(|index| {
641 index
642 .into_iter()
643 .flat_map(|v| v.clone())
644 .filter_map(Maybe::into_option)
645 .collect()
646 });
647
648 ResolverInstallerOptions {
649 index: default_index.combine(index),
650 index_url: index_args.index_url.and_then(Maybe::into_option),
651 extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
652 extra_index_url
653 .into_iter()
654 .filter_map(Maybe::into_option)
655 .collect()
656 }),
657 no_index: if index_args.no_index {
658 Some(true)
659 } else {
660 None
661 },
662 find_links: index_args.find_links.map(|find_links| {
663 find_links
664 .into_iter()
665 .filter_map(Maybe::into_option)
666 .collect()
667 }),
668 upgrade: Upgrade::from_args(
669 flag(upgrade, no_upgrade, "upgrade"),
670 upgrade_package.into_iter().map(Requirement::from).collect(),
671 upgrade_group,
672 ),
673 reinstall: Reinstall::from_args(
674 flag(reinstall, no_reinstall, "reinstall"),
675 reinstall_package,
676 ),
677 index_strategy,
678 keyring_provider,
679 resolution,
680 prerelease: if pre {
681 Some(PrereleaseMode::Allow)
682 } else {
683 prerelease
684 },
685 fork_strategy,
686 dependency_metadata: None,
687 config_settings: config_setting
688 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
689 config_settings_package: config_settings_package.map(|config_settings| {
690 config_settings
691 .into_iter()
692 .collect::<PackageConfigSettings>()
693 }),
694 build_isolation: BuildIsolation::from_args(
695 flag(no_build_isolation, build_isolation, "build-isolation"),
696 no_build_isolation_package,
697 ),
698 extra_build_dependencies: None,
699 extra_build_variables: None,
700 exclude_newer,
701 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
702 link_mode,
703 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
704 no_build: flag(no_build, build, "build"),
705 no_build_package: if no_build_package.is_empty() {
706 None
707 } else {
708 Some(no_build_package)
709 },
710 no_binary: flag(no_binary, binary, "binary"),
711 no_binary_package: if no_binary_package.is_empty() {
712 None
713 } else {
714 Some(no_binary_package)
715 },
716 no_sources: if no_sources { Some(true) } else { None },
717 no_sources_package: if no_sources_package.is_empty() {
718 None
719 } else {
720 Some(no_sources_package)
721 },
722 torch_backend: None,
723 }
724}