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