1use std::error::Error;
2use std::fmt;
3
4use anyhow::bail;
5
6use uv_cache::Refresh;
7use uv_configuration::{BuildIsolation, Reinstall, Upgrade};
8use uv_distribution_types::{ConfigSettings, Index, PackageConfigSettings, Requirement};
9use uv_resolver::{ExcludeNewerPackage, PrereleaseMode};
10use uv_settings::{Combine, EnvFlag, PipOptions, ResolverInstallerOptions, ResolverOptions};
11use uv_warnings::owo_colors::OwoColorize;
12
13use crate::{
14 BuildOptionsArgs, FetchArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs,
15 ResolverInstallerArgs,
16};
17
18#[derive(Debug)]
20pub struct ArgumentError(String);
21
22impl fmt::Display for ArgumentError {
23 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24 self.0.fmt(formatter)
25 }
26}
27
28impl Error for ArgumentError {}
29
30pub fn flag(yes: bool, no: bool, name: &str) -> anyhow::Result<Option<bool>> {
32 match (yes, no) {
33 (true, false) => Ok(Some(true)),
34 (false, true) => Ok(Some(false)),
35 (false, false) => Ok(None),
36 (..) => {
37 bail!(ArgumentError(format!(
38 "`{}` and `{}` cannot be used together. \
39 Boolean flags on different levels are currently not supported \
40 (https://github.com/clap-rs/clap/issues/6049)",
41 format!("--{name}").green(),
42 format!("--no-{name}").green(),
43 )));
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum FlagSource {
51 Cli,
53 Env(&'static str),
55 Config,
57}
58
59impl fmt::Display for FlagSource {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::Cli => write!(f, "command-line argument"),
63 Self::Env(name) => write!(f, "environment variable `{name}`"),
64 Self::Config => write!(f, "workspace configuration"),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy)]
71pub enum Flag {
72 Disabled,
74 Enabled {
76 source: FlagSource,
77 name: &'static str,
79 },
80}
81
82impl Flag {
83 pub const fn disabled() -> Self {
85 Self::Disabled
86 }
87
88 pub const fn from_cli(name: &'static str) -> Self {
90 Self::Enabled {
91 source: FlagSource::Cli,
92 name,
93 }
94 }
95
96 pub const fn from_config(name: &'static str) -> Self {
98 Self::Enabled {
99 source: FlagSource::Config,
100 name,
101 }
102 }
103
104 pub fn is_enabled(self) -> bool {
106 matches!(self, Self::Enabled { .. })
107 }
108
109 pub fn source(self) -> Option<FlagSource> {
111 match self {
112 Self::Disabled => None,
113 Self::Enabled { source, .. } => Some(source),
114 }
115 }
116}
117
118impl From<Flag> for bool {
119 fn from(flag: Flag) -> Self {
120 flag.is_enabled()
121 }
122}
123
124pub fn resolve_flag(cli_flag: bool, name: &'static str, env_flag: EnvFlag) -> Flag {
129 if cli_flag {
130 Flag::Enabled {
131 source: FlagSource::Cli,
132 name,
133 }
134 } else if env_flag.value == Some(true) {
135 Flag::Enabled {
136 source: FlagSource::Env(env_flag.env_var),
137 name,
138 }
139 } else {
140 Flag::Disabled
141 }
142}
143
144pub fn resolve_flag_pair(
149 cli_flag: bool,
150 cli_no_flag: bool,
151 name: &'static str,
152 no_name: &'static str,
153 env_flag: Option<EnvFlag>,
154 env_no_flag: Option<EnvFlag>,
155) -> (Flag, Flag) {
156 if cli_flag || cli_no_flag {
157 (
158 if cli_flag {
159 Flag::from_cli(name)
160 } else {
161 Flag::disabled()
162 },
163 if cli_no_flag {
164 Flag::from_cli(no_name)
165 } else {
166 Flag::disabled()
167 },
168 )
169 } else {
170 (
171 env_flag.map_or_else(Flag::disabled, |env_flag| {
172 resolve_flag(false, name, env_flag)
173 }),
174 env_no_flag.map_or_else(Flag::disabled, |env_no_flag| {
175 resolve_flag(false, no_name, env_no_flag)
176 }),
177 )
178 }
179}
180
181pub fn check_conflicts(flag_a: Flag, flag_b: Flag) -> anyhow::Result<()> {
186 if let (
187 Flag::Enabled {
188 source: source_a,
189 name: name_a,
190 },
191 Flag::Enabled {
192 source: source_b,
193 name: name_b,
194 },
195 ) = (flag_a, flag_b)
196 {
197 let display_a = match source_a {
198 FlagSource::Cli => format!("`--{name_a}`"),
199 FlagSource::Env(env) => format!("`{env}` (environment variable)"),
200 FlagSource::Config => format!("`{name_a}` (workspace configuration)"),
201 };
202 let display_b = match source_b {
203 FlagSource::Cli => format!("`--{name_b}`"),
204 FlagSource::Env(env) => format!("`{env}` (environment variable)"),
205 FlagSource::Config => format!("`{name_b}` (workspace configuration)"),
206 };
207 bail!(ArgumentError(format!(
208 "the argument {} cannot be used with {}",
209 display_a.green(),
210 display_b.green()
211 )));
212 }
213 Ok(())
214}
215
216impl TryFrom<RefreshArgs> for Refresh {
217 type Error = anyhow::Error;
218
219 fn try_from(value: RefreshArgs) -> anyhow::Result<Self> {
220 let RefreshArgs {
221 refresh,
222 no_refresh,
223 refresh_package,
224 } = value;
225
226 Ok(Self::from_args(
227 flag(refresh, no_refresh, "no-refresh")?,
228 refresh_package,
229 ))
230 }
231}
232
233pub fn indexes_from_args(
235 default_index: Option<&Maybe<Index>>,
236 index: Option<&[Vec<Maybe<Index>>]>,
237) -> Option<Vec<Index>> {
238 let default_index = default_index
239 .cloned()
240 .and_then(Maybe::into_option)
241 .map(|default_index| vec![default_index]);
242 let index = index.map(|index| {
243 index
244 .iter()
245 .flatten()
246 .cloned()
247 .filter_map(Maybe::into_option)
248 .collect()
249 });
250
251 default_index.combine(index)
252}
253
254impl TryFrom<ResolverArgs> for PipOptions {
255 type Error = anyhow::Error;
256
257 fn try_from(args: ResolverArgs) -> anyhow::Result<Self> {
258 let ResolverArgs {
259 index_args,
260 upgrade,
261 no_upgrade,
262 upgrade_package,
263 upgrade_group,
264 index_strategy,
265 keyring_provider,
266 resolution,
267 prerelease,
268 pre,
269 fork_strategy,
270 config_setting,
271 config_settings_package,
272 no_build_isolation,
273 no_build_isolation_package,
274 build_isolation,
275 exclude_newer,
276 link_mode,
277 no_sources,
278 no_sources_package,
279 exclude_newer_package,
280 } = args;
281
282 if !upgrade_group.is_empty() {
283 bail!(ArgumentError(format!(
284 "`{}` is not supported in `uv pip` commands",
285 "--upgrade-group".green()
286 )));
287 }
288
289 Ok(Self {
290 upgrade: flag(upgrade, no_upgrade, "no-upgrade")?,
291 upgrade_package: Some(upgrade_package),
292 index_strategy,
293 keyring_provider,
294 resolution,
295 fork_strategy,
296 prerelease: if pre {
297 Some(PrereleaseMode::Allow)
298 } else {
299 prerelease
300 },
301 config_settings: config_setting
302 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
303 config_settings_package: config_settings_package.map(|config_settings| {
304 config_settings
305 .into_iter()
306 .collect::<PackageConfigSettings>()
307 }),
308 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
309 no_build_isolation_package: Some(no_build_isolation_package),
310 exclude_newer,
311 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
312 link_mode,
313 no_sources: if no_sources { Some(true) } else { None },
314 no_sources_package: if no_sources_package.is_empty() {
315 None
316 } else {
317 Some(no_sources_package)
318 },
319 ..Self::from(index_args)
320 })
321 }
322}
323
324impl TryFrom<InstallerArgs> for PipOptions {
325 type Error = anyhow::Error;
326
327 fn try_from(args: InstallerArgs) -> anyhow::Result<Self> {
328 let InstallerArgs {
329 index_args,
330 reinstall,
331 no_reinstall,
332 reinstall_package,
333 index_strategy,
334 keyring_provider,
335 config_setting,
336 config_settings_package,
337 no_build_isolation,
338 build_isolation,
339 exclude_newer,
340 link_mode,
341 compile_bytecode,
342 no_compile_bytecode,
343 no_sources,
344 no_sources_package,
345 exclude_newer_package,
346 } = args;
347
348 Ok(Self {
349 reinstall: flag(reinstall, no_reinstall, "reinstall")?,
350 reinstall_package: Some(reinstall_package),
351 index_strategy,
352 keyring_provider,
353 config_settings: config_setting
354 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
355 config_settings_package: config_settings_package.map(|config_settings| {
356 config_settings
357 .into_iter()
358 .collect::<PackageConfigSettings>()
359 }),
360 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
361 exclude_newer,
362 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
363 link_mode,
364 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
365 no_sources: if no_sources { Some(true) } else { None },
366 no_sources_package: if no_sources_package.is_empty() {
367 None
368 } else {
369 Some(no_sources_package)
370 },
371 ..Self::from(index_args)
372 })
373 }
374}
375
376impl TryFrom<ResolverInstallerArgs> for PipOptions {
377 type Error = anyhow::Error;
378
379 fn try_from(args: ResolverInstallerArgs) -> anyhow::Result<Self> {
380 let ResolverInstallerArgs {
381 index_args,
382 upgrade,
383 no_upgrade,
384 upgrade_package,
385 upgrade_group,
386 reinstall,
387 no_reinstall,
388 reinstall_package,
389 index_strategy,
390 keyring_provider,
391 resolution,
392 prerelease,
393 pre,
394 fork_strategy,
395 config_setting,
396 config_settings_package,
397 no_build_isolation,
398 no_build_isolation_package,
399 build_isolation,
400 exclude_newer,
401 link_mode,
402 compile_bytecode,
403 no_compile_bytecode,
404 no_sources,
405 no_sources_package,
406 exclude_newer_package,
407 } = args;
408
409 if !upgrade_group.is_empty() {
410 bail!(ArgumentError(format!(
411 "`{}` is not supported in `uv pip` commands",
412 "--upgrade-group".green()
413 )));
414 }
415
416 Ok(Self {
417 upgrade: flag(upgrade, no_upgrade, "upgrade")?,
418 upgrade_package: Some(upgrade_package),
419 reinstall: flag(reinstall, no_reinstall, "reinstall")?,
420 reinstall_package: Some(reinstall_package),
421 index_strategy,
422 keyring_provider,
423 resolution,
424 prerelease: if pre {
425 Some(PrereleaseMode::Allow)
426 } else {
427 prerelease
428 },
429 fork_strategy,
430 config_settings: config_setting
431 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
432 config_settings_package: config_settings_package.map(|config_settings| {
433 config_settings
434 .into_iter()
435 .collect::<PackageConfigSettings>()
436 }),
437 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
438 no_build_isolation_package: Some(no_build_isolation_package),
439 exclude_newer,
440 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
441 link_mode,
442 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
443 no_sources: if no_sources { Some(true) } else { None },
444 no_sources_package: if no_sources_package.is_empty() {
445 None
446 } else {
447 Some(no_sources_package)
448 },
449 ..Self::from(index_args)
450 })
451 }
452}
453
454impl From<FetchArgs> for PipOptions {
455 fn from(args: FetchArgs) -> Self {
456 let FetchArgs {
457 index_args,
458 index_strategy,
459 keyring_provider,
460 exclude_newer,
461 } = args;
462
463 Self {
464 index_strategy,
465 keyring_provider,
466 exclude_newer,
467 ..Self::from(index_args)
468 }
469 }
470}
471
472impl From<IndexArgs> for PipOptions {
473 fn from(args: IndexArgs) -> Self {
474 let IndexArgs {
475 default_index,
476 index,
477 index_url,
478 extra_index_url,
479 no_index,
480 find_links,
481 } = args;
482
483 Self {
484 index: indexes_from_args(default_index.as_ref(), index.as_deref()),
485 index_url: index_url.and_then(Maybe::into_option),
486 extra_index_url: extra_index_url.map(|extra_index_urls| {
487 extra_index_urls
488 .into_iter()
489 .filter_map(Maybe::into_option)
490 .collect()
491 }),
492 no_index: if no_index { Some(true) } else { None },
493 find_links: find_links.map(|find_links| {
494 find_links
495 .into_iter()
496 .filter_map(Maybe::into_option)
497 .collect()
498 }),
499 ..Self::default()
500 }
501 }
502}
503
504pub fn resolver_options(
506 resolver_args: ResolverArgs,
507 build_args: BuildOptionsArgs,
508) -> anyhow::Result<ResolverOptions> {
509 let ResolverArgs {
510 index_args,
511 upgrade,
512 no_upgrade,
513 upgrade_package,
514 upgrade_group,
515 index_strategy,
516 keyring_provider,
517 resolution,
518 prerelease,
519 pre,
520 fork_strategy,
521 config_setting,
522 config_settings_package,
523 no_build_isolation,
524 no_build_isolation_package,
525 build_isolation,
526 exclude_newer,
527 link_mode,
528 no_sources,
529 no_sources_package,
530 exclude_newer_package,
531 } = resolver_args;
532
533 let BuildOptionsArgs {
534 no_build,
535 build,
536 no_build_package,
537 no_binary,
538 binary,
539 no_binary_package,
540 } = build_args;
541
542 Ok(ResolverOptions {
543 index: indexes_from_args(
544 index_args.default_index.as_ref(),
545 index_args.index.as_deref(),
546 ),
547 index_url: index_args.index_url.and_then(Maybe::into_option),
548 extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
549 extra_index_url
550 .into_iter()
551 .filter_map(Maybe::into_option)
552 .collect()
553 }),
554 no_index: if index_args.no_index {
555 Some(true)
556 } else {
557 None
558 },
559 find_links: index_args.find_links.map(|find_links| {
560 find_links
561 .into_iter()
562 .filter_map(Maybe::into_option)
563 .collect()
564 }),
565 upgrade: Upgrade::from_args(
566 flag(upgrade, no_upgrade, "no-upgrade")?,
567 upgrade_package.into_iter().map(Requirement::from).collect(),
568 upgrade_group,
569 ),
570 index_strategy,
571 keyring_provider,
572 resolution,
573 prerelease: if pre {
574 Some(PrereleaseMode::Allow)
575 } else {
576 prerelease
577 },
578 fork_strategy,
579 dependency_metadata: None,
580 config_settings: config_setting
581 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
582 config_settings_package: config_settings_package.map(|config_settings| {
583 config_settings
584 .into_iter()
585 .collect::<PackageConfigSettings>()
586 }),
587 build_isolation: BuildIsolation::from_args(
588 flag(no_build_isolation, build_isolation, "build-isolation")?,
589 no_build_isolation_package,
590 ),
591 extra_build_dependencies: None,
592 extra_build_variables: None,
593 exclude_newer,
594 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
595 link_mode,
596 torch_backend: None,
597 no_build: flag(no_build, build, "build")?,
598 no_build_package: if no_build_package.is_empty() {
599 None
600 } else {
601 Some(no_build_package)
602 },
603 no_binary: flag(no_binary, binary, "binary")?,
604 no_binary_package: if no_binary_package.is_empty() {
605 None
606 } else {
607 Some(no_binary_package)
608 },
609 no_sources: if no_sources { Some(true) } else { None },
610 no_sources_package: if no_sources_package.is_empty() {
611 None
612 } else {
613 Some(no_sources_package)
614 },
615 })
616}
617
618pub fn resolver_installer_options(
620 resolver_installer_args: ResolverInstallerArgs,
621 build_args: BuildOptionsArgs,
622) -> anyhow::Result<ResolverInstallerOptions> {
623 let index = indexes_from_args(
624 resolver_installer_args.index_args.default_index.as_ref(),
625 resolver_installer_args.index_args.index.as_deref(),
626 );
627 resolver_installer_options_with_indexes(resolver_installer_args, build_args, index)
628}
629
630pub fn resolver_installer_options_with_indexes(
632 resolver_installer_args: ResolverInstallerArgs,
633 build_args: BuildOptionsArgs,
634 index: Option<Vec<Index>>,
635) -> anyhow::Result<ResolverInstallerOptions> {
636 let ResolverInstallerArgs {
637 index_args,
638 upgrade,
639 no_upgrade,
640 upgrade_package,
641 upgrade_group,
642 reinstall,
643 no_reinstall,
644 reinstall_package,
645 index_strategy,
646 keyring_provider,
647 resolution,
648 prerelease,
649 pre,
650 fork_strategy,
651 config_setting,
652 config_settings_package,
653 no_build_isolation,
654 no_build_isolation_package,
655 build_isolation,
656 exclude_newer,
657 exclude_newer_package,
658 link_mode,
659 compile_bytecode,
660 no_compile_bytecode,
661 no_sources,
662 no_sources_package,
663 } = resolver_installer_args;
664
665 let BuildOptionsArgs {
666 no_build,
667 build,
668 no_build_package,
669 no_binary,
670 binary,
671 no_binary_package,
672 } = build_args;
673
674 Ok(ResolverInstallerOptions {
675 index,
676 index_url: index_args.index_url.and_then(Maybe::into_option),
677 extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
678 extra_index_url
679 .into_iter()
680 .filter_map(Maybe::into_option)
681 .collect()
682 }),
683 no_index: if index_args.no_index {
684 Some(true)
685 } else {
686 None
687 },
688 find_links: index_args.find_links.map(|find_links| {
689 find_links
690 .into_iter()
691 .filter_map(Maybe::into_option)
692 .collect()
693 }),
694 upgrade: Upgrade::from_args(
695 flag(upgrade, no_upgrade, "upgrade")?,
696 upgrade_package.into_iter().map(Requirement::from).collect(),
697 upgrade_group,
698 ),
699 reinstall: Reinstall::from_args(
700 flag(reinstall, no_reinstall, "reinstall")?,
701 reinstall_package,
702 ),
703 index_strategy,
704 keyring_provider,
705 resolution,
706 prerelease: if pre {
707 Some(PrereleaseMode::Allow)
708 } else {
709 prerelease
710 },
711 fork_strategy,
712 dependency_metadata: None,
713 config_settings: config_setting
714 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
715 config_settings_package: config_settings_package.map(|config_settings| {
716 config_settings
717 .into_iter()
718 .collect::<PackageConfigSettings>()
719 }),
720 build_isolation: BuildIsolation::from_args(
721 flag(no_build_isolation, build_isolation, "build-isolation")?,
722 no_build_isolation_package,
723 ),
724 extra_build_dependencies: None,
725 extra_build_variables: None,
726 exclude_newer,
727 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
728 link_mode,
729 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
730 no_build: flag(no_build, build, "build")?,
731 no_build_package: if no_build_package.is_empty() {
732 None
733 } else {
734 Some(no_build_package)
735 },
736 no_binary: flag(no_binary, binary, "binary")?,
737 no_binary_package: if no_binary_package.is_empty() {
738 None
739 } else {
740 Some(no_binary_package)
741 },
742 no_sources: if no_sources { Some(true) } else { None },
743 no_sources_package: if no_sources_package.is_empty() {
744 None
745 } else {
746 Some(no_sources_package)
747 },
748 torch_backend: None,
749 })
750}