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 BuildIsolationArgs, BuildOptionsArgs, CompileBytecodeArgs, ExcludeNewerArgs, FetchArgs,
15 IndexArgs, InstallerArgs, Maybe, PackageBuildIsolationArgs, PackageExcludeNewerArgs,
16 RefreshArgs, RegistryClientArgs, ReinstallArgs, ResolverArgs, ResolverInstallerArgs,
17 SourcesArgs, VersionSelectionArgs,
18};
19
20#[derive(Debug)]
22pub struct ArgumentError(String);
23
24impl fmt::Display for ArgumentError {
25 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26 self.0.fmt(formatter)
27 }
28}
29
30impl Error for ArgumentError {}
31
32pub fn flag(yes: bool, no: bool, name: &str) -> anyhow::Result<Option<bool>> {
34 match (yes, no) {
35 (true, false) => Ok(Some(true)),
36 (false, true) => Ok(Some(false)),
37 (false, false) => Ok(None),
38 (..) => {
39 bail!(ArgumentError(format!(
40 "`{}` and `{}` cannot be used together. \
41 Boolean flags on different levels are currently not supported \
42 (https://github.com/clap-rs/clap/issues/6049)",
43 format!("--{name}").green(),
44 format!("--no-{name}").green(),
45 )));
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum FlagSource {
53 Cli,
55 Env(&'static str),
57 Config,
59}
60
61impl fmt::Display for FlagSource {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Cli => write!(f, "command-line argument"),
65 Self::Env(name) => write!(f, "environment variable `{name}`"),
66 Self::Config => write!(f, "workspace configuration"),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy)]
73pub enum Flag {
74 Disabled,
76 Enabled {
78 source: FlagSource,
79 name: &'static str,
81 },
82}
83
84impl Flag {
85 pub const fn disabled() -> Self {
87 Self::Disabled
88 }
89
90 pub const fn from_cli(name: &'static str) -> Self {
92 Self::Enabled {
93 source: FlagSource::Cli,
94 name,
95 }
96 }
97
98 pub const fn from_config(name: &'static str) -> Self {
100 Self::Enabled {
101 source: FlagSource::Config,
102 name,
103 }
104 }
105
106 pub fn is_enabled(self) -> bool {
108 matches!(self, Self::Enabled { .. })
109 }
110
111 pub fn source(self) -> Option<FlagSource> {
113 match self {
114 Self::Disabled => None,
115 Self::Enabled { source, .. } => Some(source),
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) -> anyhow::Result<()> {
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 bail!(ArgumentError(format!(
210 "the argument {} cannot be used with {}",
211 display_a.green(),
212 display_b.green()
213 )));
214 }
215 Ok(())
216}
217
218impl TryFrom<RefreshArgs> for Refresh {
219 type Error = anyhow::Error;
220
221 fn try_from(value: RefreshArgs) -> anyhow::Result<Self> {
222 let RefreshArgs {
223 refresh,
224 no_refresh,
225 refresh_package,
226 } = value;
227
228 Ok(Self::from_args(
229 flag(refresh, no_refresh, "no-refresh")?,
230 refresh_package,
231 ))
232 }
233}
234
235pub fn indexes_from_args(
237 default_index: Option<&Maybe<Index>>,
238 index: Option<&[Vec<Maybe<Index>>]>,
239) -> Option<Vec<Index>> {
240 let default_index = default_index
241 .cloned()
242 .and_then(Maybe::into_option)
243 .map(|default_index| vec![default_index]);
244 let index = index.map(|index| {
245 index
246 .iter()
247 .flatten()
248 .cloned()
249 .filter_map(Maybe::into_option)
250 .collect()
251 });
252
253 default_index.combine(index)
254}
255
256impl TryFrom<ResolverArgs> for PipOptions {
257 type Error = anyhow::Error;
258
259 fn try_from(args: ResolverArgs) -> anyhow::Result<Self> {
260 let ResolverArgs {
261 index_args,
262 upgrade,
263 no_upgrade,
264 upgrade_package,
265 upgrade_group,
266 registry_client:
267 RegistryClientArgs {
268 index_strategy,
269 keyring_provider,
270 },
271 version_selection:
272 VersionSelectionArgs {
273 resolution,
274 prerelease,
275 pre,
276 fork_strategy,
277 },
278 config_setting,
279 config_settings_package,
280 build_isolation:
281 PackageBuildIsolationArgs {
282 build_isolation:
283 BuildIsolationArgs {
284 no_build_isolation,
285 build_isolation,
286 },
287 no_build_isolation_package,
288 },
289 exclude_newer:
290 PackageExcludeNewerArgs {
291 exclude_newer: ExcludeNewerArgs { exclude_newer },
292 exclude_newer_package,
293 },
294 link_mode,
295 sources:
296 SourcesArgs {
297 no_sources,
298 no_sources_package,
299 },
300 } = args;
301
302 if !upgrade_group.is_empty() {
303 bail!(ArgumentError(format!(
304 "`{}` is not supported in `uv pip` commands",
305 "--upgrade-group".green()
306 )));
307 }
308
309 Ok(Self {
310 upgrade: flag(upgrade, no_upgrade, "no-upgrade")?,
311 upgrade_package: Some(upgrade_package),
312 index_strategy,
313 keyring_provider,
314 resolution,
315 fork_strategy,
316 prerelease: if pre {
317 Some(PrereleaseMode::Allow)
318 } else {
319 prerelease
320 },
321 config_settings: config_setting
322 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
323 config_settings_package: config_settings_package.map(|config_settings| {
324 config_settings
325 .into_iter()
326 .collect::<PackageConfigSettings>()
327 }),
328 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
329 no_build_isolation_package: Some(no_build_isolation_package),
330 exclude_newer,
331 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
332 link_mode,
333 no_sources: if no_sources { Some(true) } else { None },
334 no_sources_package: if no_sources_package.is_empty() {
335 None
336 } else {
337 Some(no_sources_package)
338 },
339 ..Self::from(index_args)
340 })
341 }
342}
343
344impl TryFrom<InstallerArgs> for PipOptions {
345 type Error = anyhow::Error;
346
347 fn try_from(args: InstallerArgs) -> anyhow::Result<Self> {
348 let InstallerArgs {
349 index_args,
350 reinstall:
351 ReinstallArgs {
352 reinstall,
353 no_reinstall,
354 reinstall_package,
355 },
356 registry_client:
357 RegistryClientArgs {
358 index_strategy,
359 keyring_provider,
360 },
361 config_setting,
362 config_settings_package,
363 build_isolation:
364 BuildIsolationArgs {
365 no_build_isolation,
366 build_isolation,
367 },
368 exclude_newer:
369 PackageExcludeNewerArgs {
370 exclude_newer: ExcludeNewerArgs { exclude_newer },
371 exclude_newer_package,
372 },
373 link_mode,
374 compile_bytecode:
375 CompileBytecodeArgs {
376 compile_bytecode,
377 no_compile_bytecode,
378 },
379 sources:
380 SourcesArgs {
381 no_sources,
382 no_sources_package,
383 },
384 } = args;
385
386 Ok(Self {
387 reinstall: flag(reinstall, no_reinstall, "reinstall")?,
388 reinstall_package: Some(reinstall_package),
389 index_strategy,
390 keyring_provider,
391 config_settings: config_setting
392 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
393 config_settings_package: config_settings_package.map(|config_settings| {
394 config_settings
395 .into_iter()
396 .collect::<PackageConfigSettings>()
397 }),
398 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
399 exclude_newer,
400 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
401 link_mode,
402 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
403 no_sources: if no_sources { Some(true) } else { None },
404 no_sources_package: if no_sources_package.is_empty() {
405 None
406 } else {
407 Some(no_sources_package)
408 },
409 ..Self::from(index_args)
410 })
411 }
412}
413
414impl TryFrom<ResolverInstallerArgs> for PipOptions {
415 type Error = anyhow::Error;
416
417 fn try_from(args: ResolverInstallerArgs) -> anyhow::Result<Self> {
418 let ResolverInstallerArgs {
419 index_args,
420 upgrade,
421 no_upgrade,
422 upgrade_package,
423 upgrade_group,
424 reinstall:
425 ReinstallArgs {
426 reinstall,
427 no_reinstall,
428 reinstall_package,
429 },
430 registry_client:
431 RegistryClientArgs {
432 index_strategy,
433 keyring_provider,
434 },
435 version_selection:
436 VersionSelectionArgs {
437 resolution,
438 prerelease,
439 pre,
440 fork_strategy,
441 },
442 config_setting,
443 config_settings_package,
444 build_isolation:
445 PackageBuildIsolationArgs {
446 build_isolation:
447 BuildIsolationArgs {
448 no_build_isolation,
449 build_isolation,
450 },
451 no_build_isolation_package,
452 },
453 exclude_newer:
454 PackageExcludeNewerArgs {
455 exclude_newer: ExcludeNewerArgs { exclude_newer },
456 exclude_newer_package,
457 },
458 link_mode,
459 compile_bytecode:
460 CompileBytecodeArgs {
461 compile_bytecode,
462 no_compile_bytecode,
463 },
464 sources:
465 SourcesArgs {
466 no_sources,
467 no_sources_package,
468 },
469 } = args;
470
471 if !upgrade_group.is_empty() {
472 bail!(ArgumentError(format!(
473 "`{}` is not supported in `uv pip` commands",
474 "--upgrade-group".green()
475 )));
476 }
477
478 Ok(Self {
479 upgrade: flag(upgrade, no_upgrade, "upgrade")?,
480 upgrade_package: Some(upgrade_package),
481 reinstall: flag(reinstall, no_reinstall, "reinstall")?,
482 reinstall_package: Some(reinstall_package),
483 index_strategy,
484 keyring_provider,
485 resolution,
486 prerelease: if pre {
487 Some(PrereleaseMode::Allow)
488 } else {
489 prerelease
490 },
491 fork_strategy,
492 config_settings: config_setting
493 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
494 config_settings_package: config_settings_package.map(|config_settings| {
495 config_settings
496 .into_iter()
497 .collect::<PackageConfigSettings>()
498 }),
499 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
500 no_build_isolation_package: Some(no_build_isolation_package),
501 exclude_newer,
502 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
503 link_mode,
504 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
505 no_sources: if no_sources { Some(true) } else { None },
506 no_sources_package: if no_sources_package.is_empty() {
507 None
508 } else {
509 Some(no_sources_package)
510 },
511 ..Self::from(index_args)
512 })
513 }
514}
515
516impl From<FetchArgs> for PipOptions {
517 fn from(args: FetchArgs) -> Self {
518 let FetchArgs {
519 index_args,
520 registry_client:
521 RegistryClientArgs {
522 index_strategy,
523 keyring_provider,
524 },
525 exclude_newer:
526 PackageExcludeNewerArgs {
527 exclude_newer: ExcludeNewerArgs { exclude_newer },
528 exclude_newer_package,
529 },
530 } = args;
531
532 Self {
533 index_strategy,
534 keyring_provider,
535 exclude_newer,
536 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
537 ..Self::from(index_args)
538 }
539 }
540}
541
542impl From<IndexArgs> for PipOptions {
543 fn from(args: IndexArgs) -> Self {
544 let IndexArgs {
545 default_index,
546 index,
547 index_url,
548 extra_index_url,
549 no_index,
550 find_links,
551 } = args;
552
553 Self {
554 index: indexes_from_args(default_index.as_ref(), index.as_deref()),
555 index_url: index_url.and_then(Maybe::into_option),
556 extra_index_url: extra_index_url.map(|extra_index_urls| {
557 extra_index_urls
558 .into_iter()
559 .filter_map(Maybe::into_option)
560 .collect()
561 }),
562 no_index: if no_index { Some(true) } else { None },
563 find_links: find_links.map(|find_links| {
564 find_links
565 .into_iter()
566 .filter_map(Maybe::into_option)
567 .collect()
568 }),
569 ..Self::default()
570 }
571 }
572}
573
574pub fn resolver_options(
576 resolver_args: ResolverArgs,
577 build_args: BuildOptionsArgs,
578) -> anyhow::Result<ResolverOptions> {
579 let ResolverArgs {
580 index_args,
581 upgrade,
582 no_upgrade,
583 upgrade_package,
584 upgrade_group,
585 registry_client:
586 RegistryClientArgs {
587 index_strategy,
588 keyring_provider,
589 },
590 version_selection:
591 VersionSelectionArgs {
592 resolution,
593 prerelease,
594 pre,
595 fork_strategy,
596 },
597 config_setting,
598 config_settings_package,
599 build_isolation:
600 PackageBuildIsolationArgs {
601 build_isolation:
602 BuildIsolationArgs {
603 no_build_isolation,
604 build_isolation,
605 },
606 no_build_isolation_package,
607 },
608 exclude_newer:
609 PackageExcludeNewerArgs {
610 exclude_newer: ExcludeNewerArgs { exclude_newer },
611 exclude_newer_package,
612 },
613 link_mode,
614 sources: SourcesArgs {
615 no_sources,
616 no_sources_package,
617 },
618 } = resolver_args;
619
620 let BuildOptionsArgs {
621 no_build,
622 build,
623 no_build_package,
624 no_binary,
625 binary,
626 no_binary_package,
627 } = build_args;
628
629 Ok(ResolverOptions {
630 index: indexes_from_args(
631 index_args.default_index.as_ref(),
632 index_args.index.as_deref(),
633 ),
634 index_url: index_args.index_url.and_then(Maybe::into_option),
635 extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
636 extra_index_url
637 .into_iter()
638 .filter_map(Maybe::into_option)
639 .collect()
640 }),
641 no_index: if index_args.no_index {
642 Some(true)
643 } else {
644 None
645 },
646 find_links: index_args.find_links.map(|find_links| {
647 find_links
648 .into_iter()
649 .filter_map(Maybe::into_option)
650 .collect()
651 }),
652 upgrade: Upgrade::from_args(
653 flag(upgrade, no_upgrade, "no-upgrade")?,
654 upgrade_package.into_iter().map(Requirement::from).collect(),
655 upgrade_group,
656 ),
657 index_strategy,
658 keyring_provider,
659 resolution,
660 prerelease: if pre {
661 Some(PrereleaseMode::Allow)
662 } else {
663 prerelease
664 },
665 fork_strategy,
666 dependency_metadata: None,
667 config_settings: config_setting
668 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
669 config_settings_package: config_settings_package.map(|config_settings| {
670 config_settings
671 .into_iter()
672 .collect::<PackageConfigSettings>()
673 }),
674 build_isolation: BuildIsolation::from_args(
675 flag(no_build_isolation, build_isolation, "build-isolation")?,
676 no_build_isolation_package,
677 ),
678 extra_build_dependencies: None,
679 extra_build_variables: None,
680 exclude_newer,
681 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
682 link_mode,
683 torch_backend: None,
684 no_build: flag(no_build, build, "build")?,
685 no_build_package: if no_build_package.is_empty() {
686 None
687 } else {
688 Some(no_build_package)
689 },
690 no_binary: flag(no_binary, binary, "binary")?,
691 no_binary_package: if no_binary_package.is_empty() {
692 None
693 } else {
694 Some(no_binary_package)
695 },
696 no_sources: if no_sources { Some(true) } else { None },
697 no_sources_package: if no_sources_package.is_empty() {
698 None
699 } else {
700 Some(no_sources_package)
701 },
702 })
703}
704
705pub fn resolver_installer_options(
707 resolver_installer_args: ResolverInstallerArgs,
708 build_args: BuildOptionsArgs,
709) -> anyhow::Result<ResolverInstallerOptions> {
710 let index = indexes_from_args(
711 resolver_installer_args.index_args.default_index.as_ref(),
712 resolver_installer_args.index_args.index.as_deref(),
713 );
714 resolver_installer_options_with_indexes(resolver_installer_args, build_args, index)
715}
716
717pub fn resolver_installer_options_with_indexes(
719 resolver_installer_args: ResolverInstallerArgs,
720 build_args: BuildOptionsArgs,
721 index: Option<Vec<Index>>,
722) -> anyhow::Result<ResolverInstallerOptions> {
723 let ResolverInstallerArgs {
724 index_args,
725 upgrade,
726 no_upgrade,
727 upgrade_package,
728 upgrade_group,
729 reinstall:
730 ReinstallArgs {
731 reinstall,
732 no_reinstall,
733 reinstall_package,
734 },
735 registry_client:
736 RegistryClientArgs {
737 index_strategy,
738 keyring_provider,
739 },
740 version_selection:
741 VersionSelectionArgs {
742 resolution,
743 prerelease,
744 pre,
745 fork_strategy,
746 },
747 config_setting,
748 config_settings_package,
749 build_isolation:
750 PackageBuildIsolationArgs {
751 build_isolation:
752 BuildIsolationArgs {
753 no_build_isolation,
754 build_isolation,
755 },
756 no_build_isolation_package,
757 },
758 exclude_newer:
759 PackageExcludeNewerArgs {
760 exclude_newer: ExcludeNewerArgs { exclude_newer },
761 exclude_newer_package,
762 },
763 link_mode,
764 compile_bytecode:
765 CompileBytecodeArgs {
766 compile_bytecode,
767 no_compile_bytecode,
768 },
769 sources: SourcesArgs {
770 no_sources,
771 no_sources_package,
772 },
773 } = resolver_installer_args;
774
775 let BuildOptionsArgs {
776 no_build,
777 build,
778 no_build_package,
779 no_binary,
780 binary,
781 no_binary_package,
782 } = build_args;
783
784 Ok(ResolverInstallerOptions {
785 index,
786 index_url: index_args.index_url.and_then(Maybe::into_option),
787 extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
788 extra_index_url
789 .into_iter()
790 .filter_map(Maybe::into_option)
791 .collect()
792 }),
793 no_index: if index_args.no_index {
794 Some(true)
795 } else {
796 None
797 },
798 find_links: index_args.find_links.map(|find_links| {
799 find_links
800 .into_iter()
801 .filter_map(Maybe::into_option)
802 .collect()
803 }),
804 upgrade: Upgrade::from_args(
805 flag(upgrade, no_upgrade, "upgrade")?,
806 upgrade_package.into_iter().map(Requirement::from).collect(),
807 upgrade_group,
808 ),
809 reinstall: Reinstall::from_args(
810 flag(reinstall, no_reinstall, "reinstall")?,
811 reinstall_package,
812 ),
813 index_strategy,
814 keyring_provider,
815 resolution,
816 prerelease: if pre {
817 Some(PrereleaseMode::Allow)
818 } else {
819 prerelease
820 },
821 fork_strategy,
822 dependency_metadata: None,
823 config_settings: config_setting
824 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
825 config_settings_package: config_settings_package.map(|config_settings| {
826 config_settings
827 .into_iter()
828 .collect::<PackageConfigSettings>()
829 }),
830 build_isolation: BuildIsolation::from_args(
831 flag(no_build_isolation, build_isolation, "build-isolation")?,
832 no_build_isolation_package,
833 ),
834 extra_build_dependencies: None,
835 extra_build_variables: None,
836 exclude_newer,
837 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
838 link_mode,
839 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
840 no_build: flag(no_build, build, "build")?,
841 no_build_package: if no_build_package.is_empty() {
842 None
843 } else {
844 Some(no_build_package)
845 },
846 no_binary: flag(no_binary, binary, "binary")?,
847 no_binary_package: if no_binary_package.is_empty() {
848 None
849 } else {
850 Some(no_binary_package)
851 },
852 no_sources: if no_sources { Some(true) } else { None },
853 no_sources_package: if no_sources_package.is_empty() {
854 None
855 } else {
856 Some(no_sources_package)
857 },
858 torch_backend: None,
859 })
860}