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::{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,
588 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
589 link_mode,
590 torch_backend: None,
591 no_build: flag(no_build, build, "build"),
592 no_build_package: if no_build_package.is_empty() {
593 None
594 } else {
595 Some(no_build_package)
596 },
597 no_binary: flag(no_binary, binary, "binary"),
598 no_binary_package: if no_binary_package.is_empty() {
599 None
600 } else {
601 Some(no_binary_package)
602 },
603 no_sources: if no_sources { Some(true) } else { None },
604 no_sources_package: if no_sources_package.is_empty() {
605 None
606 } else {
607 Some(no_sources_package)
608 },
609 }
610}
611
612pub fn resolver_installer_options(
614 resolver_installer_args: ResolverInstallerArgs,
615 build_args: BuildOptionsArgs,
616) -> ResolverInstallerOptions {
617 let index = indexes_from_args(
618 resolver_installer_args.index_args.default_index.as_ref(),
619 resolver_installer_args.index_args.index.as_deref(),
620 );
621 resolver_installer_options_with_indexes(resolver_installer_args, build_args, index)
622}
623
624pub fn resolver_installer_options_with_indexes(
626 resolver_installer_args: ResolverInstallerArgs,
627 build_args: BuildOptionsArgs,
628 index: Option<Vec<Index>>,
629) -> ResolverInstallerOptions {
630 let ResolverInstallerArgs {
631 index_args,
632 upgrade,
633 no_upgrade,
634 upgrade_package,
635 upgrade_group,
636 reinstall,
637 no_reinstall,
638 reinstall_package,
639 index_strategy,
640 keyring_provider,
641 resolution,
642 prerelease,
643 pre,
644 fork_strategy,
645 config_setting,
646 config_settings_package,
647 no_build_isolation,
648 no_build_isolation_package,
649 build_isolation,
650 exclude_newer,
651 exclude_newer_package,
652 link_mode,
653 compile_bytecode,
654 no_compile_bytecode,
655 no_sources,
656 no_sources_package,
657 } = resolver_installer_args;
658
659 let BuildOptionsArgs {
660 no_build,
661 build,
662 no_build_package,
663 no_binary,
664 binary,
665 no_binary_package,
666 } = build_args;
667
668 ResolverInstallerOptions {
669 index,
670 index_url: index_args.index_url.and_then(Maybe::into_option),
671 extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
672 extra_index_url
673 .into_iter()
674 .filter_map(Maybe::into_option)
675 .collect()
676 }),
677 no_index: if index_args.no_index {
678 Some(true)
679 } else {
680 None
681 },
682 find_links: index_args.find_links.map(|find_links| {
683 find_links
684 .into_iter()
685 .filter_map(Maybe::into_option)
686 .collect()
687 }),
688 upgrade: Upgrade::from_args(
689 flag(upgrade, no_upgrade, "upgrade"),
690 upgrade_package.into_iter().map(Requirement::from).collect(),
691 upgrade_group,
692 ),
693 reinstall: Reinstall::from_args(
694 flag(reinstall, no_reinstall, "reinstall"),
695 reinstall_package,
696 ),
697 index_strategy,
698 keyring_provider,
699 resolution,
700 prerelease: if pre {
701 Some(PrereleaseMode::Allow)
702 } else {
703 prerelease
704 },
705 fork_strategy,
706 dependency_metadata: None,
707 config_settings: config_setting
708 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
709 config_settings_package: config_settings_package.map(|config_settings| {
710 config_settings
711 .into_iter()
712 .collect::<PackageConfigSettings>()
713 }),
714 build_isolation: BuildIsolation::from_args(
715 flag(no_build_isolation, build_isolation, "build-isolation"),
716 no_build_isolation_package,
717 ),
718 extra_build_dependencies: None,
719 extra_build_variables: None,
720 exclude_newer,
721 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
722 link_mode,
723 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
724 no_build: flag(no_build, build, "build"),
725 no_build_package: if no_build_package.is_empty() {
726 None
727 } else {
728 Some(no_build_package)
729 },
730 no_binary: flag(no_binary, binary, "binary"),
731 no_binary_package: if no_binary_package.is_empty() {
732 None
733 } else {
734 Some(no_binary_package)
735 },
736 no_sources: if no_sources { Some(true) } else { None },
737 no_sources_package: if no_sources_package.is_empty() {
738 None
739 } else {
740 Some(no_sources_package)
741 },
742 torch_backend: None,
743 }
744}