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, 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
243impl TryFrom<ResolverArgs> for PipOptions {
244 type Error = anyhow::Error;
245
246 fn try_from(args: ResolverArgs) -> anyhow::Result<Self> {
247 let ResolverArgs {
248 index_args,
249 upgrade,
250 no_upgrade,
251 upgrade_package,
252 upgrade_group,
253 registry_client:
254 RegistryClientArgs {
255 index_strategy,
256 keyring_provider,
257 },
258 version_selection:
259 VersionSelectionArgs {
260 resolution,
261 prerelease,
262 prerelease_package,
263 pre,
264 fork_strategy,
265 },
266 config_setting,
267 config_settings_package,
268 build_isolation:
269 PackageBuildIsolationArgs {
270 build_isolation:
271 BuildIsolationArgs {
272 no_build_isolation,
273 build_isolation,
274 },
275 no_build_isolation_package,
276 },
277 exclude_newer:
278 PackageExcludeNewerArgs {
279 exclude_newer: ExcludeNewerArgs { exclude_newer },
280 exclude_newer_package,
281 },
282 link_mode,
283 sources:
284 SourcesArgs {
285 no_sources,
286 no_sources_package,
287 },
288 } = args;
289
290 if !upgrade_group.is_empty() {
291 bail!(ArgumentError(format!(
292 "`{}` is not supported in `uv pip` commands",
293 "--upgrade-group".green()
294 )));
295 }
296
297 Ok(Self {
298 upgrade: flag(upgrade, no_upgrade, "upgrade")?,
299 upgrade_package: Some(upgrade_package),
300 index_strategy,
301 keyring_provider,
302 resolution,
303 fork_strategy,
304 prerelease: if pre {
305 Some(PrereleaseMode::Allow)
306 } else {
307 prerelease
308 },
309 prerelease_package: prerelease_package.map(PrereleasePackage::from_iter),
310 config_settings: config_setting
311 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
312 config_settings_package: config_settings_package.map(|config_settings| {
313 config_settings
314 .into_iter()
315 .collect::<PackageConfigSettings>()
316 }),
317 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
318 no_build_isolation_package: Some(no_build_isolation_package),
319 exclude_newer,
320 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
321 link_mode,
322 no_sources: if no_sources { Some(true) } else { None },
323 no_sources_package: if no_sources_package.is_empty() {
324 None
325 } else {
326 Some(no_sources_package)
327 },
328 ..Self::try_from(index_args)?
329 })
330 }
331}
332
333impl TryFrom<InstallerArgs> for PipOptions {
334 type Error = anyhow::Error;
335
336 fn try_from(args: InstallerArgs) -> anyhow::Result<Self> {
337 let InstallerArgs {
338 index_args,
339 reinstall:
340 ReinstallArgs {
341 reinstall,
342 no_reinstall,
343 reinstall_package,
344 },
345 registry_client:
346 RegistryClientArgs {
347 index_strategy,
348 keyring_provider,
349 },
350 config_setting,
351 config_settings_package,
352 build_isolation:
353 BuildIsolationArgs {
354 no_build_isolation,
355 build_isolation,
356 },
357 exclude_newer:
358 PackageExcludeNewerArgs {
359 exclude_newer: ExcludeNewerArgs { exclude_newer },
360 exclude_newer_package,
361 },
362 link_mode,
363 compile_bytecode:
364 CompileBytecodeArgs {
365 compile_bytecode,
366 no_compile_bytecode,
367 },
368 sources:
369 SourcesArgs {
370 no_sources,
371 no_sources_package,
372 },
373 } = args;
374
375 Ok(Self {
376 reinstall: flag(reinstall, no_reinstall, "reinstall")?,
377 reinstall_package: Some(reinstall_package),
378 index_strategy,
379 keyring_provider,
380 config_settings: config_setting
381 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
382 config_settings_package: config_settings_package.map(|config_settings| {
383 config_settings
384 .into_iter()
385 .collect::<PackageConfigSettings>()
386 }),
387 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
388 exclude_newer,
389 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
390 link_mode,
391 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
392 no_sources: if no_sources { Some(true) } else { None },
393 no_sources_package: if no_sources_package.is_empty() {
394 None
395 } else {
396 Some(no_sources_package)
397 },
398 ..Self::try_from(index_args)?
399 })
400 }
401}
402
403impl TryFrom<ResolverInstallerArgs> for PipOptions {
404 type Error = anyhow::Error;
405
406 fn try_from(args: ResolverInstallerArgs) -> anyhow::Result<Self> {
407 let ResolverInstallerArgs {
408 index_args,
409 upgrade,
410 no_upgrade,
411 upgrade_package,
412 upgrade_group,
413 reinstall:
414 ReinstallArgs {
415 reinstall,
416 no_reinstall,
417 reinstall_package,
418 },
419 registry_client:
420 RegistryClientArgs {
421 index_strategy,
422 keyring_provider,
423 },
424 version_selection:
425 VersionSelectionArgs {
426 resolution,
427 prerelease,
428 prerelease_package,
429 pre,
430 fork_strategy,
431 },
432 config_setting,
433 config_settings_package,
434 build_isolation:
435 PackageBuildIsolationArgs {
436 build_isolation:
437 BuildIsolationArgs {
438 no_build_isolation,
439 build_isolation,
440 },
441 no_build_isolation_package,
442 },
443 exclude_newer:
444 PackageExcludeNewerArgs {
445 exclude_newer: ExcludeNewerArgs { exclude_newer },
446 exclude_newer_package,
447 },
448 link_mode,
449 compile_bytecode:
450 CompileBytecodeArgs {
451 compile_bytecode,
452 no_compile_bytecode,
453 },
454 sources:
455 SourcesArgs {
456 no_sources,
457 no_sources_package,
458 },
459 } = args;
460
461 if !upgrade_group.is_empty() {
462 bail!(ArgumentError(format!(
463 "`{}` is not supported in `uv pip` commands",
464 "--upgrade-group".green()
465 )));
466 }
467
468 Ok(Self {
469 upgrade: flag(upgrade, no_upgrade, "upgrade")?,
470 upgrade_package: Some(upgrade_package),
471 reinstall: flag(reinstall, no_reinstall, "reinstall")?,
472 reinstall_package: Some(reinstall_package),
473 index_strategy,
474 keyring_provider,
475 resolution,
476 prerelease: if pre {
477 Some(PrereleaseMode::Allow)
478 } else {
479 prerelease
480 },
481 prerelease_package: prerelease_package.map(PrereleasePackage::from_iter),
482 fork_strategy,
483 config_settings: config_setting
484 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
485 config_settings_package: config_settings_package.map(|config_settings| {
486 config_settings
487 .into_iter()
488 .collect::<PackageConfigSettings>()
489 }),
490 no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation")?,
491 no_build_isolation_package: Some(no_build_isolation_package),
492 exclude_newer,
493 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
494 link_mode,
495 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
496 no_sources: if no_sources { Some(true) } else { None },
497 no_sources_package: if no_sources_package.is_empty() {
498 None
499 } else {
500 Some(no_sources_package)
501 },
502 ..Self::try_from(index_args)?
503 })
504 }
505}
506
507impl TryFrom<FetchArgs> for PipOptions {
508 type Error = anyhow::Error;
509
510 fn try_from(args: FetchArgs) -> anyhow::Result<Self> {
511 let FetchArgs {
512 index_args,
513 registry_client:
514 RegistryClientArgs {
515 index_strategy,
516 keyring_provider,
517 },
518 exclude_newer:
519 PackageExcludeNewerArgs {
520 exclude_newer: ExcludeNewerArgs { exclude_newer },
521 exclude_newer_package,
522 },
523 } = args;
524
525 Ok(Self {
526 index_strategy,
527 keyring_provider,
528 exclude_newer,
529 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
530 ..Self::try_from(index_args)?
531 })
532 }
533}
534
535impl IndexArgs {
536 fn resolve(self) -> IndexOptions {
538 let Self {
539 default_index,
540 index,
541 index_url,
542 extra_index_url,
543 no_index,
544 find_links,
545 } = self;
546
547 let default_index = default_index
548 .and_then(Maybe::into_option)
549 .map(|index| vec![index]);
550 let index = index.map(|indexes| {
551 indexes
552 .into_iter()
553 .flatten()
554 .filter_map(Maybe::into_option)
555 .collect()
556 });
557
558 IndexOptions {
559 index: default_index.combine(index),
560 index_url: index_url.and_then(Maybe::into_option),
561 extra_index_url: extra_index_url
562 .map(|indexes| indexes.into_iter().filter_map(Maybe::into_option).collect()),
563 no_index: no_index.then_some(true),
564 find_links: find_links
565 .map(|links| links.into_iter().filter_map(Maybe::into_option).collect()),
566 }
567 }
568}
569
570impl TryFrom<IndexArgs> for PipOptions {
571 type Error = anyhow::Error;
572
573 fn try_from(args: IndexArgs) -> anyhow::Result<Self> {
574 Ok(Self::from(
575 args.resolve().relative_to(&env::current_dir()?)?,
576 ))
577 }
578}
579
580pub fn resolver_options(
582 resolver_args: ResolverArgs,
583 build_args: BuildOptionsArgs,
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(),
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) -> anyhow::Result<ResolverInstallerOptions> {
699 let ResolverInstallerArgs {
700 index_args,
701 upgrade,
702 no_upgrade,
703 upgrade_package,
704 upgrade_group,
705 reinstall:
706 ReinstallArgs {
707 reinstall,
708 no_reinstall,
709 reinstall_package,
710 },
711 registry_client:
712 RegistryClientArgs {
713 index_strategy,
714 keyring_provider,
715 },
716 version_selection:
717 VersionSelectionArgs {
718 resolution,
719 prerelease,
720 prerelease_package,
721 pre,
722 fork_strategy,
723 },
724 config_setting,
725 config_settings_package,
726 build_isolation:
727 PackageBuildIsolationArgs {
728 build_isolation:
729 BuildIsolationArgs {
730 no_build_isolation,
731 build_isolation,
732 },
733 no_build_isolation_package,
734 },
735 exclude_newer:
736 PackageExcludeNewerArgs {
737 exclude_newer: ExcludeNewerArgs { exclude_newer },
738 exclude_newer_package,
739 },
740 link_mode,
741 compile_bytecode:
742 CompileBytecodeArgs {
743 compile_bytecode,
744 no_compile_bytecode,
745 },
746 sources: SourcesArgs {
747 no_sources,
748 no_sources_package,
749 },
750 } = resolver_installer_args;
751
752 let BuildOptionsArgs {
753 no_build,
754 build,
755 no_build_package,
756 no_binary,
757 binary,
758 no_binary_package,
759 } = build_args;
760
761 ResolverInstallerOptions {
762 indexes: index_args.resolve(),
763 upgrade: Upgrade::from_args(
764 flag(upgrade, no_upgrade, "upgrade")?,
765 upgrade_package.into_iter().map(Requirement::from).collect(),
766 upgrade_group,
767 ),
768 reinstall: Reinstall::from_args(
769 flag(reinstall, no_reinstall, "reinstall")?,
770 reinstall_package,
771 ),
772 index_strategy,
773 keyring_provider,
774 resolution,
775 prerelease: if pre {
776 Some(PrereleaseMode::Allow)
777 } else {
778 prerelease
779 },
780 prerelease_package: prerelease_package.map(PrereleasePackage::from_iter),
781 fork_strategy,
782 dependency_metadata: None,
783 config_settings: config_setting
784 .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
785 config_settings_package: config_settings_package.map(|config_settings| {
786 config_settings
787 .into_iter()
788 .collect::<PackageConfigSettings>()
789 }),
790 build_isolation: BuildIsolation::from_args(
791 flag(no_build_isolation, build_isolation, "build-isolation")?,
792 no_build_isolation_package,
793 ),
794 extra_build_dependencies: None,
795 extra_build_variables: None,
796 exclude_newer,
797 exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
798 link_mode,
799 compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode")?,
800 no_build: flag(no_build, build, "build")?,
801 no_build_package: if no_build_package.is_empty() {
802 None
803 } else {
804 Some(no_build_package)
805 },
806 no_binary: flag(no_binary, binary, "binary")?,
807 no_binary_package: if no_binary_package.is_empty() {
808 None
809 } else {
810 Some(no_binary_package)
811 },
812 no_sources: if no_sources { Some(true) } else { None },
813 no_sources_package: if no_sources_package.is_empty() {
814 None
815 } else {
816 Some(no_sources_package)
817 },
818 torch_backend: None,
819 }
820 .relative_to(&env::current_dir()?)
821 .map_err(Into::into)
822}