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 pub fn source(self) -> Option<FlagSource> {
121 match self {
122 Self::Disabled => None,
123 Self::Enabled { source, .. } => Some(source),
124 }
125 }
126}
127
128impl From<Flag> for bool {
129 fn from(flag: Flag) -> Self {
130 flag.is_enabled()
131 }
132}
133
134pub fn resolve_flag(cli_flag: bool, name: &'static str, env_flag: EnvFlag) -> Flag {
139 if cli_flag {
140 Flag::Enabled {
141 source: FlagSource::Cli,
142 name,
143 }
144 } else if env_flag.value == Some(true) {
145 Flag::Enabled {
146 source: FlagSource::Env(env_flag.env_var),
147 name,
148 }
149 } else {
150 Flag::Disabled
151 }
152}
153
154pub fn resolve_flag_pair(
159 cli_flag: bool,
160 cli_no_flag: bool,
161 name: &'static str,
162 no_name: &'static str,
163 env_flag: Option<EnvFlag>,
164 env_no_flag: Option<EnvFlag>,
165) -> (Flag, Flag) {
166 if cli_flag || cli_no_flag {
167 (
168 if cli_flag {
169 Flag::from_cli(name)
170 } else {
171 Flag::disabled()
172 },
173 if cli_no_flag {
174 Flag::from_cli(no_name)
175 } else {
176 Flag::disabled()
177 },
178 )
179 } else {
180 (
181 env_flag.map_or_else(Flag::disabled, |env_flag| {
182 resolve_flag(false, name, env_flag)
183 }),
184 env_no_flag.map_or_else(Flag::disabled, |env_no_flag| {
185 resolve_flag(false, no_name, env_no_flag)
186 }),
187 )
188 }
189}
190
191pub fn check_conflicts(flag_a: Flag, flag_b: Flag) -> anyhow::Result<()> {
196 if let (
197 Flag::Enabled {
198 source: source_a,
199 name: name_a,
200 },
201 Flag::Enabled {
202 source: source_b,
203 name: name_b,
204 },
205 ) = (flag_a, flag_b)
206 {
207 let display_a = match source_a {
208 FlagSource::Cli => format!("`--{name_a}`"),
209 FlagSource::Env(env) => format!("`{env}` (environment variable)"),
210 FlagSource::Config => format!("`{name_a}` (workspace configuration)"),
211 };
212 let display_b = match source_b {
213 FlagSource::Cli => format!("`--{name_b}`"),
214 FlagSource::Env(env) => format!("`{env}` (environment variable)"),
215 FlagSource::Config => format!("`{name_b}` (workspace configuration)"),
216 };
217 bail!(ArgumentError(format!(
218 "the argument {} cannot be used with {}",
219 display_a.green(),
220 display_b.green()
221 )));
222 }
223 Ok(())
224}
225
226impl TryFrom<RefreshArgs> for Refresh {
227 type Error = anyhow::Error;
228
229 fn try_from(value: RefreshArgs) -> anyhow::Result<Self> {
230 let RefreshArgs {
231 refresh,
232 no_refresh,
233 refresh_package,
234 } = value;
235
236 Ok(Self::from_args(
237 flag(refresh, no_refresh, "refresh")?,
238 refresh_package,
239 ))
240 }
241}
242
243pub trait IntoPipOptions {
245 fn into_pip_options(self, configured_indexes: &[Index]) -> anyhow::Result<PipOptions>;
247}
248
249impl IntoPipOptions for ResolverArgs {
250 fn into_pip_options(self, configured_indexes: &[Index]) -> anyhow::Result<PipOptions> {
252 let Self {
253 index_args,
254 upgrade,
255 no_upgrade,
256 upgrade_package,
257 upgrade_group,
258 registry_client:
259 RegistryClientArgs {
260 index_strategy,
261 keyring_provider,
262 },
263 version_selection:
264 VersionSelectionArgs {
265 resolution,
266 prerelease,
267 prerelease_package,
268 pre,
269 fork_strategy,
270 },
271 config_setting,
272 config_settings_package,
273 build_isolation:
274 PackageBuildIsolationArgs {
275 build_isolation:
276 BuildIsolationArgs {
277 no_build_isolation,
278 build_isolation,
279 },
280 no_build_isolation_package,
281 },
282 exclude_newer:
283 PackageExcludeNewerArgs {
284 exclude_newer: ExcludeNewerArgs { exclude_newer },
285 exclude_newer_package,
286 },
287 link_mode,
288 sources:
289 SourcesArgs {
290 no_sources,
291 no_sources_package,
292 },
293 } = self;
294
295 if !upgrade_group.is_empty() {
296 bail!(ArgumentError(format!(
297 "`{}` is not supported in `uv pip` commands",
298 "--upgrade-group".green()
299 )));
300 }
301
302 Ok(PipOptions {
303 upgrade: flag(upgrade, no_upgrade, "upgrade")?,
304 upgrade_package: Some(upgrade_package),
305 index_strategy,
306 keyring_provider,
307 resolution,
308 fork_strategy,
309 prerelease: if pre {
310 Some(PrereleaseMode::Allow)
311 } else {
312 prerelease
313 },
314 prerelease_package: prerelease_package.map(PrereleasePackage::from_iter),
315 config_settings: config_setting
316 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
317 config_settings_package: config_settings_package.map(|config_settings| {
318 config_settings
319 .into_iter()
320 .collect::<PackageConfigSettings>()
321 }),
322 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
323 no_build_isolation_package: Some(no_build_isolation_package),
324 exclude_newer,
325 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
326 link_mode,
327 no_sources: if no_sources { Some(true) } else { None },
328 no_sources_package: if no_sources_package.is_empty() {
329 None
330 } else {
331 Some(no_sources_package)
332 },
333 ..index_args.into_pip_options(configured_indexes)?
334 })
335 }
336}
337
338impl IntoPipOptions for InstallerArgs {
339 fn into_pip_options(self, configured_indexes: &[Index]) -> anyhow::Result<PipOptions> {
341 let Self {
342 index_args,
343 reinstall:
344 ReinstallArgs {
345 reinstall,
346 no_reinstall,
347 reinstall_package,
348 },
349 registry_client:
350 RegistryClientArgs {
351 index_strategy,
352 keyring_provider,
353 },
354 config_setting,
355 config_settings_package,
356 build_isolation:
357 BuildIsolationArgs {
358 no_build_isolation,
359 build_isolation,
360 },
361 exclude_newer:
362 PackageExcludeNewerArgs {
363 exclude_newer: ExcludeNewerArgs { exclude_newer },
364 exclude_newer_package,
365 },
366 link_mode,
367 compile_bytecode:
368 CompileBytecodeArgs {
369 compile_bytecode,
370 no_compile_bytecode,
371 },
372 sources:
373 SourcesArgs {
374 no_sources,
375 no_sources_package,
376 },
377 } = self;
378
379 Ok(PipOptions {
380 reinstall: flag(reinstall, no_reinstall, "reinstall")?,
381 reinstall_package: Some(reinstall_package),
382 index_strategy,
383 keyring_provider,
384 config_settings: config_setting
385 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
386 config_settings_package: config_settings_package.map(|config_settings| {
387 config_settings
388 .into_iter()
389 .collect::<PackageConfigSettings>()
390 }),
391 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
392 exclude_newer,
393 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
394 link_mode,
395 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
396 no_sources: if no_sources { Some(true) } else { None },
397 no_sources_package: if no_sources_package.is_empty() {
398 None
399 } else {
400 Some(no_sources_package)
401 },
402 ..index_args.into_pip_options(configured_indexes)?
403 })
404 }
405}
406
407impl IntoPipOptions for ResolverInstallerArgs {
408 fn into_pip_options(self, configured_indexes: &[Index]) -> anyhow::Result<PipOptions> {
410 let Self {
411 index_args,
412 upgrade,
413 no_upgrade,
414 upgrade_package,
415 upgrade_group,
416 reinstall:
417 ReinstallArgs {
418 reinstall,
419 no_reinstall,
420 reinstall_package,
421 },
422 registry_client:
423 RegistryClientArgs {
424 index_strategy,
425 keyring_provider,
426 },
427 version_selection:
428 VersionSelectionArgs {
429 resolution,
430 prerelease,
431 prerelease_package,
432 pre,
433 fork_strategy,
434 },
435 config_setting,
436 config_settings_package,
437 build_isolation:
438 PackageBuildIsolationArgs {
439 build_isolation:
440 BuildIsolationArgs {
441 no_build_isolation,
442 build_isolation,
443 },
444 no_build_isolation_package,
445 },
446 exclude_newer:
447 PackageExcludeNewerArgs {
448 exclude_newer: ExcludeNewerArgs { exclude_newer },
449 exclude_newer_package,
450 },
451 link_mode,
452 compile_bytecode:
453 CompileBytecodeArgs {
454 compile_bytecode,
455 no_compile_bytecode,
456 },
457 sources:
458 SourcesArgs {
459 no_sources,
460 no_sources_package,
461 },
462 } = self;
463
464 if !upgrade_group.is_empty() {
465 bail!(ArgumentError(format!(
466 "`{}` is not supported in `uv pip` commands",
467 "--upgrade-group".green()
468 )));
469 }
470
471 Ok(PipOptions {
472 upgrade: flag(upgrade, no_upgrade, "upgrade")?,
473 upgrade_package: Some(upgrade_package),
474 reinstall: flag(reinstall, no_reinstall, "reinstall")?,
475 reinstall_package: Some(reinstall_package),
476 index_strategy,
477 keyring_provider,
478 resolution,
479 prerelease: if pre {
480 Some(PrereleaseMode::Allow)
481 } else {
482 prerelease
483 },
484 prerelease_package: prerelease_package.map(PrereleasePackage::from_iter),
485 fork_strategy,
486 config_settings: config_setting
487 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
488 config_settings_package: config_settings_package.map(|config_settings| {
489 config_settings
490 .into_iter()
491 .collect::<PackageConfigSettings>()
492 }),
493 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
494 no_build_isolation_package: Some(no_build_isolation_package),
495 exclude_newer,
496 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
497 link_mode,
498 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
499 no_sources: if no_sources { Some(true) } else { None },
500 no_sources_package: if no_sources_package.is_empty() {
501 None
502 } else {
503 Some(no_sources_package)
504 },
505 ..index_args.into_pip_options(configured_indexes)?
506 })
507 }
508}
509
510impl IntoPipOptions for FetchArgs {
511 fn into_pip_options(self, configured_indexes: &[Index]) -> anyhow::Result<PipOptions> {
513 let Self {
514 index_args,
515 registry_client:
516 RegistryClientArgs {
517 index_strategy,
518 keyring_provider,
519 },
520 exclude_newer:
521 PackageExcludeNewerArgs {
522 exclude_newer: ExcludeNewerArgs { exclude_newer },
523 exclude_newer_package,
524 },
525 } = self;
526
527 Ok(PipOptions {
528 index_strategy,
529 keyring_provider,
530 exclude_newer,
531 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
532 ..index_args.into_pip_options(configured_indexes)?
533 })
534 }
535}
536
537impl IndexArgs {
538 fn resolve(self, configured_indexes: &[Index]) -> anyhow::Result<IndexOptions> {
540 let Self {
541 default_index,
542 index,
543 index_url,
544 extra_index_url,
545 no_index,
546 find_links,
547 } = self;
548
549 let default_index = default_index
550 .and_then(Maybe::into_option)
551 .map(|index| index.resolve(configured_indexes))
552 .transpose()?
553 .map(|index| vec![index]);
554 let index = index
555 .map(|indexes| {
556 indexes
557 .into_iter()
558 .flatten()
559 .filter_map(Maybe::into_option)
560 .map(|index| index.resolve(configured_indexes))
561 .collect::<anyhow::Result<Vec<_>>>()
562 })
563 .transpose()?;
564
565 Ok(IndexOptions {
566 index: default_index.combine(index),
567 index_url: index_url.and_then(Maybe::into_option),
568 extra_index_url: extra_index_url
569 .map(|indexes| indexes.into_iter().filter_map(Maybe::into_option).collect()),
570 no_index: no_index.then_some(true),
571 find_links: find_links
572 .map(|links| links.into_iter().filter_map(Maybe::into_option).collect()),
573 })
574 }
575}
576
577impl IntoPipOptions for IndexArgs {
578 fn into_pip_options(self, configured_indexes: &[Index]) -> anyhow::Result<PipOptions> {
580 Ok(PipOptions::from(
581 self.resolve(configured_indexes)?
582 .relative_to(&env::current_dir()?)?,
583 ))
584 }
585}
586
587pub fn resolver_options(
589 resolver_args: ResolverArgs,
590 build_args: BuildOptionsArgs,
591 configured_indexes: &[Index],
592) -> anyhow::Result<ResolverOptions> {
593 let ResolverArgs {
594 index_args,
595 upgrade,
596 no_upgrade,
597 upgrade_package,
598 upgrade_group,
599 registry_client:
600 RegistryClientArgs {
601 index_strategy,
602 keyring_provider,
603 },
604 version_selection:
605 VersionSelectionArgs {
606 resolution,
607 prerelease,
608 prerelease_package,
609 pre,
610 fork_strategy,
611 },
612 config_setting,
613 config_settings_package,
614 build_isolation:
615 PackageBuildIsolationArgs {
616 build_isolation:
617 BuildIsolationArgs {
618 no_build_isolation,
619 build_isolation,
620 },
621 no_build_isolation_package,
622 },
623 exclude_newer:
624 PackageExcludeNewerArgs {
625 exclude_newer: ExcludeNewerArgs { exclude_newer },
626 exclude_newer_package,
627 },
628 link_mode,
629 sources: SourcesArgs {
630 no_sources,
631 no_sources_package,
632 },
633 } = resolver_args;
634
635 let BuildOptionsArgs {
636 no_build,
637 build,
638 no_build_package,
639 no_binary,
640 binary,
641 no_binary_package,
642 } = build_args;
643
644 ResolverOptions {
645 indexes: index_args.resolve(configured_indexes)?,
646 upgrade: Upgrade::from_args(
647 flag(upgrade, no_upgrade, "upgrade")?,
648 upgrade_package.into_iter().map(Requirement::from).collect(),
649 upgrade_group,
650 ),
651 index_strategy,
652 keyring_provider,
653 resolution,
654 prerelease: if pre {
655 Some(PrereleaseMode::Allow)
656 } else {
657 prerelease
658 },
659 prerelease_package: prerelease_package.map(PrereleasePackage::from_iter),
660 fork_strategy,
661 dependency_metadata: None,
662 config_settings: config_setting
663 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
664 config_settings_package: config_settings_package.map(|config_settings| {
665 config_settings
666 .into_iter()
667 .collect::<PackageConfigSettings>()
668 }),
669 build_isolation: BuildIsolation::from_args(
670 flag(no_build_isolation, build_isolation, "build-isolation")?,
671 no_build_isolation_package,
672 ),
673 extra_build_dependencies: None,
674 extra_build_variables: None,
675 exclude_newer,
676 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
677 link_mode,
678 torch_backend: None,
679 no_build: flag(no_build, build, "build")?,
680 no_build_package: if no_build_package.is_empty() {
681 None
682 } else {
683 Some(no_build_package)
684 },
685 no_binary: flag(no_binary, binary, "binary")?,
686 no_binary_package: if no_binary_package.is_empty() {
687 None
688 } else {
689 Some(no_binary_package)
690 },
691 no_sources: if no_sources { Some(true) } else { None },
692 no_sources_package: if no_sources_package.is_empty() {
693 None
694 } else {
695 Some(no_sources_package)
696 },
697 }
698 .relative_to(&env::current_dir()?)
699 .map_err(Into::into)
700}
701
702pub fn resolver_installer_options(
704 resolver_installer_args: ResolverInstallerArgs,
705 build_args: BuildOptionsArgs,
706 configured_indexes: &[Index],
707) -> anyhow::Result<ResolverInstallerOptions> {
708 let ResolverInstallerArgs {
709 index_args,
710 upgrade,
711 no_upgrade,
712 upgrade_package,
713 upgrade_group,
714 reinstall:
715 ReinstallArgs {
716 reinstall,
717 no_reinstall,
718 reinstall_package,
719 },
720 registry_client:
721 RegistryClientArgs {
722 index_strategy,
723 keyring_provider,
724 },
725 version_selection:
726 VersionSelectionArgs {
727 resolution,
728 prerelease,
729 prerelease_package,
730 pre,
731 fork_strategy,
732 },
733 config_setting,
734 config_settings_package,
735 build_isolation:
736 PackageBuildIsolationArgs {
737 build_isolation:
738 BuildIsolationArgs {
739 no_build_isolation,
740 build_isolation,
741 },
742 no_build_isolation_package,
743 },
744 exclude_newer:
745 PackageExcludeNewerArgs {
746 exclude_newer: ExcludeNewerArgs { exclude_newer },
747 exclude_newer_package,
748 },
749 link_mode,
750 compile_bytecode:
751 CompileBytecodeArgs {
752 compile_bytecode,
753 no_compile_bytecode,
754 },
755 sources: SourcesArgs {
756 no_sources,
757 no_sources_package,
758 },
759 } = resolver_installer_args;
760
761 let BuildOptionsArgs {
762 no_build,
763 build,
764 no_build_package,
765 no_binary,
766 binary,
767 no_binary_package,
768 } = build_args;
769
770 ResolverInstallerOptions {
771 indexes: index_args.resolve(configured_indexes)?,
772 upgrade: Upgrade::from_args(
773 flag(upgrade, no_upgrade, "upgrade")?,
774 upgrade_package.into_iter().map(Requirement::from).collect(),
775 upgrade_group,
776 ),
777 reinstall: Reinstall::from_args(
778 flag(reinstall, no_reinstall, "reinstall")?,
779 reinstall_package,
780 ),
781 index_strategy,
782 keyring_provider,
783 resolution,
784 prerelease: if pre {
785 Some(PrereleaseMode::Allow)
786 } else {
787 prerelease
788 },
789 prerelease_package: prerelease_package.map(PrereleasePackage::from_iter),
790 fork_strategy,
791 dependency_metadata: None,
792 config_settings: config_setting
793 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
794 config_settings_package: config_settings_package.map(|config_settings| {
795 config_settings
796 .into_iter()
797 .collect::<PackageConfigSettings>()
798 }),
799 build_isolation: BuildIsolation::from_args(
800 flag(no_build_isolation, build_isolation, "build-isolation")?,
801 no_build_isolation_package,
802 ),
803 extra_build_dependencies: None,
804 extra_build_variables: None,
805 exclude_newer,
806 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
807 link_mode,
808 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
809 no_build: flag(no_build, build, "build")?,
810 no_build_package: if no_build_package.is_empty() {
811 None
812 } else {
813 Some(no_build_package)
814 },
815 no_binary: flag(no_binary, binary, "binary")?,
816 no_binary_package: if no_binary_package.is_empty() {
817 None
818 } else {
819 Some(no_binary_package)
820 },
821 no_sources: if no_sources { Some(true) } else { None },
822 no_sources_package: if no_sources_package.is_empty() {
823 None
824 } else {
825 Some(no_sources_package)
826 },
827 torch_backend: None,
828 }
829 .relative_to(&env::current_dir()?)
830 .map_err(Into::into)
831}