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