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