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