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