1#[cfg(feature = "schemars")]
10use std::borrow::Cow;
11use std::collections::BTreeMap;
12use std::fmt::Formatter;
13use std::ops::Deref;
14use std::path::{Path, PathBuf};
15use std::str::FromStr;
16
17use glob::Pattern;
18use owo_colors::OwoColorize;
19use rustc_hash::{FxBuildHasher, FxHashSet};
20use serde::de::{IntoDeserializer, SeqAccess};
21use serde::{Deserialize, Deserializer, Serialize};
22use thiserror::Error;
23
24use uv_build_backend::BuildBackendSettings;
25use uv_configuration::GitLfsSetting;
26use uv_distribution_types::{Index, IndexName, RequirementSource};
27use uv_fs::{PortablePathBuf, relative_to};
28use uv_git_types::GitReference;
29use uv_macros::OptionsMetadata;
30use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName};
31use uv_options_metadata::{OptionSet, OptionsMetadata, Visit};
32use uv_pep440::{Version, VersionSpecifiers};
33use uv_pep508::MarkerTree;
34use uv_pypi_types::{
35 Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl,
36};
37use uv_redacted::DisplaySafeUrl;
38
39#[derive(Error, Debug)]
40pub enum PyprojectTomlError {
41 #[error(transparent)]
42 TomlSyntax(#[from] toml_edit::TomlError),
43 #[error(transparent)]
44 TomlSchema(#[from] toml_edit::de::Error),
45 #[error(
46 "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
47 )]
48 MissingName,
49 #[error(
50 "`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list"
51 )]
52 MissingVersion,
53}
54
55fn deserialize_unique_map<'de, D, K, V, F>(
57 deserializer: D,
58 error_msg: F,
59) -> Result<BTreeMap<K, V>, D::Error>
60where
61 D: Deserializer<'de>,
62 K: Deserialize<'de> + Ord + std::fmt::Display,
63 V: Deserialize<'de>,
64 F: FnOnce(&K) -> String,
65{
66 struct Visitor<K, V, F>(F, std::marker::PhantomData<(K, V)>);
67
68 impl<'de, K, V, F> serde::de::Visitor<'de> for Visitor<K, V, F>
69 where
70 K: Deserialize<'de> + Ord + std::fmt::Display,
71 V: Deserialize<'de>,
72 F: FnOnce(&K) -> String,
73 {
74 type Value = BTreeMap<K, V>;
75
76 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
77 formatter.write_str("a map with unique keys")
78 }
79
80 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
81 where
82 M: serde::de::MapAccess<'de>,
83 {
84 use std::collections::btree_map::Entry;
85
86 let mut map = BTreeMap::new();
87 while let Some((key, value)) = access.next_entry::<K, V>()? {
88 match map.entry(key) {
89 Entry::Occupied(entry) => {
90 return Err(serde::de::Error::custom((self.0)(entry.key())));
91 }
92 Entry::Vacant(entry) => {
93 entry.insert(value);
94 }
95 }
96 }
97 Ok(map)
98 }
99 }
100
101 deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData))
102}
103
104#[derive(Deserialize, Debug, Clone)]
106#[cfg_attr(test, derive(Serialize))]
107#[serde(rename_all = "kebab-case")]
108pub struct PyProjectToml {
109 pub project: Option<Project>,
111 pub tool: Option<Tool>,
113 pub dependency_groups: Option<DependencyGroups>,
115 #[serde(skip)]
117 pub raw: String,
118
119 #[serde(default, skip_serializing)]
121 pub build_system: Option<serde::de::IgnoredAny>,
122}
123
124impl PyProjectToml {
125 pub fn from_string(raw: String) -> Result<Self, PyprojectTomlError> {
127 let pyproject =
128 toml_edit::Document::from_str(&raw).map_err(PyprojectTomlError::TomlSyntax)?;
129 let pyproject = Self::deserialize(pyproject.into_deserializer())
130 .map_err(PyprojectTomlError::TomlSchema)?;
131 Ok(Self { raw, ..pyproject })
132 }
133
134 pub fn is_package(&self, require_build_system: bool) -> bool {
137 if let Some(is_package) = self.tool_uv_package() {
139 return is_package;
140 }
141
142 self.build_system.is_some() || !require_build_system
144 }
145
146 fn tool_uv_package(&self) -> Option<bool> {
148 self.tool
149 .as_ref()
150 .and_then(|tool| tool.uv.as_ref())
151 .and_then(|uv| uv.package)
152 }
153
154 pub fn is_dynamic(&self) -> bool {
156 self.project
157 .as_ref()
158 .is_some_and(|project| project.version.is_none())
159 }
160
161 pub fn has_scripts(&self) -> bool {
163 if let Some(ref project) = self.project {
164 project.gui_scripts.is_some() || project.scripts.is_some()
165 } else {
166 false
167 }
168 }
169
170 pub fn conflicts(&self) -> Conflicts {
172 let empty = Conflicts::empty();
173 let Some(project) = self.project.as_ref() else {
174 return empty;
175 };
176 let Some(tool) = self.tool.as_ref() else {
177 return empty;
178 };
179 let Some(tooluv) = tool.uv.as_ref() else {
180 return empty;
181 };
182 let Some(conflicting) = tooluv.conflicts.as_ref() else {
183 return empty;
184 };
185 conflicting.to_conflicts_with_package_name(&project.name)
186 }
187}
188
189impl PartialEq for PyProjectToml {
191 fn eq(&self, other: &Self) -> bool {
192 self.project.eq(&other.project) && self.tool.eq(&other.tool)
193 }
194}
195
196impl Eq for PyProjectToml {}
197
198impl AsRef<[u8]> for PyProjectToml {
199 fn as_ref(&self) -> &[u8] {
200 self.raw.as_bytes()
201 }
202}
203
204#[derive(Deserialize, Debug, Clone, PartialEq)]
208#[cfg_attr(test, derive(Serialize))]
209#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
210pub struct Project {
211 pub name: PackageName,
213 pub version: Option<Version>,
215 pub requires_python: Option<VersionSpecifiers>,
217 pub dependencies: Option<Vec<String>>,
219 pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
221
222 #[serde(default, skip_serializing)]
224 pub(crate) gui_scripts: Option<serde::de::IgnoredAny>,
225 #[serde(default, skip_serializing)]
227 pub(crate) scripts: Option<serde::de::IgnoredAny>,
228}
229
230#[derive(Deserialize, Debug)]
231#[serde(rename_all = "kebab-case")]
232struct ProjectWire {
233 name: Option<PackageName>,
234 version: Option<Version>,
235 dynamic: Option<Vec<String>>,
236 requires_python: Option<VersionSpecifiers>,
237 dependencies: Option<Vec<String>>,
238 optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
239 gui_scripts: Option<serde::de::IgnoredAny>,
240 scripts: Option<serde::de::IgnoredAny>,
241}
242
243impl TryFrom<ProjectWire> for Project {
244 type Error = PyprojectTomlError;
245
246 fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
247 let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
249
250 if value.version.is_none()
252 && !value
253 .dynamic
254 .as_ref()
255 .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
256 {
257 return Err(PyprojectTomlError::MissingVersion);
258 }
259
260 Ok(Self {
261 name,
262 version: value.version,
263 requires_python: value.requires_python,
264 dependencies: value.dependencies,
265 optional_dependencies: value.optional_dependencies,
266 gui_scripts: value.gui_scripts,
267 scripts: value.scripts,
268 })
269 }
270}
271
272#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
273#[cfg_attr(test, derive(Serialize))]
274#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
275pub struct Tool {
276 pub uv: Option<ToolUv>,
277}
278
279fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
284where
285 D: Deserializer<'de>,
286{
287 let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
288 if let Some(indexes) = indexes.as_ref() {
289 let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
290 for index in indexes {
291 if let Some(name) = index.name.as_ref() {
292 if !seen_names.insert(name) {
293 return Err(serde::de::Error::custom(format!(
294 "duplicate index name `{name}`"
295 )));
296 }
297 }
298 }
299 }
300 Ok(indexes)
301}
302
303#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
306#[cfg_attr(test, derive(Serialize))]
307#[serde(rename_all = "kebab-case")]
308#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
309pub struct ToolUv {
310 #[option(
318 default = "{}",
319 value_type = "dict",
320 example = r#"
321 [tool.uv.sources]
322 httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
323 pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
324 pydantic = { path = "/path/to/pydantic", editable = true }
325 "#
326 )]
327 pub sources: Option<ToolUvSources>,
328
329 #[option(
357 default = "[]",
358 value_type = "dict",
359 example = r#"
360 [[tool.uv.index]]
361 name = "pytorch"
362 url = "https://download.pytorch.org/whl/cu121"
363 "#
364 )]
365 #[serde(deserialize_with = "deserialize_index_vec", default)]
366 pub index: Option<Vec<Index>>,
367
368 #[option_group]
370 pub workspace: Option<ToolUvWorkspace>,
371
372 #[option(
375 default = r#"true"#,
376 value_type = "bool",
377 example = r#"
378 managed = false
379 "#
380 )]
381 pub managed: Option<bool>,
382
383 #[option(
394 default = r#"true"#,
395 value_type = "bool",
396 example = r#"
397 package = false
398 "#
399 )]
400 pub package: Option<bool>,
401
402 #[option(
406 default = r#"["dev"]"#,
407 value_type = r#"str | list[str]"#,
408 example = r#"
409 default-groups = ["docs"]
410 "#
411 )]
412 pub default_groups: Option<DefaultGroups>,
413
414 #[option(
423 default = "[]",
424 value_type = "dict",
425 example = r#"
426 [tool.uv.dependency-groups]
427 my-group = {requires-python = ">=3.12"}
428 "#
429 )]
430 pub dependency_groups: Option<ToolUvDependencyGroups>,
431
432 #[cfg_attr(
442 feature = "schemars",
443 schemars(
444 with = "Option<Vec<String>>",
445 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
446 )
447 )]
448 #[option(
449 default = "[]",
450 value_type = "list[str]",
451 example = r#"
452 dev-dependencies = ["ruff==0.5.0"]
453 "#
454 )]
455 pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
456
457 #[cfg_attr(
476 feature = "schemars",
477 schemars(
478 with = "Option<Vec<String>>",
479 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
480 )
481 )]
482 #[option(
483 default = "[]",
484 value_type = "list[str]",
485 example = r#"
486 # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
487 # a different version.
488 override-dependencies = ["werkzeug==2.3.0"]
489 "#
490 )]
491 pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
492
493 #[cfg_attr(
508 feature = "schemars",
509 schemars(
510 with = "Option<Vec<String>>",
511 description = "Package names to exclude, e.g., `werkzeug`, `numpy`."
512 )
513 )]
514 #[option(
515 default = "[]",
516 value_type = "list[str]",
517 example = r#"
518 # Exclude Werkzeug from being installed, even if transitive dependencies request it.
519 exclude-dependencies = ["werkzeug"]
520 "#
521 )]
522 pub exclude_dependencies: Option<Vec<PackageName>>,
523
524 #[cfg_attr(
538 feature = "schemars",
539 schemars(
540 with = "Option<Vec<String>>",
541 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
542 )
543 )]
544 #[option(
545 default = "[]",
546 value_type = "list[str]",
547 example = r#"
548 # Ensure that the grpcio version is always less than 1.65, if it's requested by a
549 # direct or transitive dependency.
550 constraint-dependencies = ["grpcio<1.65"]
551 "#
552 )]
553 pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
554
555 #[cfg_attr(
569 feature = "schemars",
570 schemars(
571 with = "Option<Vec<String>>",
572 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
573 )
574 )]
575 #[option(
576 default = "[]",
577 value_type = "list[str]",
578 example = r#"
579 # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
580 # on setuptools.
581 build-constraint-dependencies = ["setuptools==60.0.0"]
582 "#
583 )]
584 pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
585
586 #[cfg_attr(
595 feature = "schemars",
596 schemars(
597 with = "Option<Vec<String>>",
598 description = "A list of environment markers, e.g., `python_version >= '3.6'`."
599 )
600 )]
601 #[option(
602 default = "[]",
603 value_type = "str | list[str]",
604 example = r#"
605 # Resolve for macOS, but not for Linux or Windows.
606 environments = ["sys_platform == 'darwin'"]
607 "#
608 )]
609 pub environments: Option<SupportedEnvironments>,
610
611 #[cfg_attr(
631 feature = "schemars",
632 schemars(
633 with = "Option<Vec<String>>",
634 description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
635 )
636 )]
637 #[option(
638 default = "[]",
639 value_type = "str | list[str]",
640 example = r#"
641 # Require that the package is available for macOS ARM and x86 (Intel).
642 required-environments = [
643 "sys_platform == 'darwin' and platform_machine == 'arm64'",
644 "sys_platform == 'darwin' and platform_machine == 'x86_64'",
645 ]
646 "#
647 )]
648 pub required_environments: Option<SupportedEnvironments>,
649
650 #[cfg_attr(
665 feature = "schemars",
666 schemars(description = "A list of sets of conflicting groups or extras.")
667 )]
668 #[option(
669 default = r#"[]"#,
670 value_type = "list[list[dict]]",
671 example = r#"
672 # Require that `package[extra1]` and `package[extra2]` are resolved
673 # in different forks so that they cannot conflict with one another.
674 conflicts = [
675 [
676 { extra = "extra1" },
677 { extra = "extra2" },
678 ]
679 ]
680
681 # Require that the dependency groups `group1` and `group2`
682 # are resolved in different forks so that they cannot conflict
683 # with one another.
684 conflicts = [
685 [
686 { group = "group1" },
687 { group = "group2" },
688 ]
689 ]
690 "#
691 )]
692 pub conflicts: Option<SchemaConflicts>,
693
694 #[option_group]
701 pub build_backend: Option<BuildBackendSettingsSchema>,
702}
703
704#[derive(Default, Debug, Clone, PartialEq, Eq)]
705#[cfg_attr(test, derive(Serialize))]
706#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
707pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
708
709impl ToolUvSources {
710 pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
712 &self.0
713 }
714
715 #[must_use]
717 pub fn into_inner(self) -> BTreeMap<PackageName, Sources> {
718 self.0
719 }
720}
721
722impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
724 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
725 where
726 D: Deserializer<'de>,
727 {
728 deserialize_unique_map(deserializer, |key: &PackageName| {
729 format!("duplicate sources for package `{key}`")
730 })
731 .map(ToolUvSources)
732 }
733}
734
735#[derive(Default, Debug, Clone, PartialEq, Eq)]
736#[cfg_attr(test, derive(Serialize))]
737#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
738pub struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
739
740impl ToolUvDependencyGroups {
741 pub fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
743 &self.0
744 }
745
746 #[must_use]
748 pub fn into_inner(self) -> BTreeMap<GroupName, DependencyGroupSettings> {
749 self.0
750 }
751}
752
753impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
755 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
756 where
757 D: Deserializer<'de>,
758 {
759 deserialize_unique_map(deserializer, |key: &GroupName| {
760 format!("duplicate settings for dependency group `{key}`")
761 })
762 .map(ToolUvDependencyGroups)
763 }
764}
765
766#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
767#[cfg_attr(test, derive(Serialize))]
768#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
769#[serde(rename_all = "kebab-case")]
770pub struct DependencyGroupSettings {
771 #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
773 pub requires_python: Option<VersionSpecifiers>,
774}
775
776#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
777#[serde(untagged, rename_all = "kebab-case")]
778#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
779pub enum ExtraBuildDependencyWire {
780 Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
781 #[serde(rename_all = "kebab-case")]
782 Annotated {
783 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
784 match_runtime: bool,
785 },
786}
787
788#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
789#[serde(
790 deny_unknown_fields,
791 from = "ExtraBuildDependencyWire",
792 into = "ExtraBuildDependencyWire"
793)]
794#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
795pub struct ExtraBuildDependency {
796 pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
797 pub match_runtime: bool,
798}
799
800impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
801 fn from(value: ExtraBuildDependency) -> Self {
802 value.requirement
803 }
804}
805
806impl From<ExtraBuildDependencyWire> for ExtraBuildDependency {
807 fn from(wire: ExtraBuildDependencyWire) -> Self {
808 match wire {
809 ExtraBuildDependencyWire::Unannotated(requirement) => Self {
810 requirement,
811 match_runtime: false,
812 },
813 ExtraBuildDependencyWire::Annotated {
814 requirement,
815 match_runtime,
816 } => Self {
817 requirement,
818 match_runtime,
819 },
820 }
821 }
822}
823
824impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
825 fn from(item: ExtraBuildDependency) -> Self {
826 Self::Annotated {
827 requirement: item.requirement,
828 match_runtime: item.match_runtime,
829 }
830 }
831}
832
833#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
834#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
835pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
836
837impl std::ops::Deref for ExtraBuildDependencies {
838 type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
839
840 fn deref(&self) -> &Self::Target {
841 &self.0
842 }
843}
844
845impl std::ops::DerefMut for ExtraBuildDependencies {
846 fn deref_mut(&mut self) -> &mut Self::Target {
847 &mut self.0
848 }
849}
850
851impl IntoIterator for ExtraBuildDependencies {
852 type Item = (PackageName, Vec<ExtraBuildDependency>);
853 type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
854
855 fn into_iter(self) -> Self::IntoIter {
856 self.0.into_iter()
857 }
858}
859
860impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
861 fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
862 iter: T,
863 ) -> Self {
864 Self(iter.into_iter().collect())
865 }
866}
867
868impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
870 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
871 where
872 D: Deserializer<'de>,
873 {
874 deserialize_unique_map(deserializer, |key: &PackageName| {
875 format!("duplicate extra-build-dependencies for `{key}`")
876 })
877 .map(ExtraBuildDependencies)
878 }
879}
880
881#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
882#[cfg_attr(test, derive(Serialize))]
883#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
884#[serde(rename_all = "kebab-case", deny_unknown_fields)]
885pub struct ToolUvWorkspace {
886 #[option(
892 default = "[]",
893 value_type = "list[str]",
894 example = r#"
895 members = ["member1", "path/to/member2", "libs/*"]
896 "#
897 )]
898 pub members: Option<Vec<SerdePattern>>,
899 #[option(
906 default = "[]",
907 value_type = "list[str]",
908 example = r#"
909 exclude = ["member1", "path/to/member2", "libs/*"]
910 "#
911 )]
912 pub exclude: Option<Vec<SerdePattern>>,
913}
914
915#[derive(Debug, Clone, PartialEq, Eq)]
917pub struct SerdePattern(Pattern);
918
919impl serde::ser::Serialize for SerdePattern {
920 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
921 where
922 S: serde::ser::Serializer,
923 {
924 self.0.as_str().serialize(serializer)
925 }
926}
927
928impl<'de> serde::Deserialize<'de> for SerdePattern {
929 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
930 struct Visitor;
931
932 impl serde::de::Visitor<'_> for Visitor {
933 type Value = SerdePattern;
934
935 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
936 f.write_str("a string")
937 }
938
939 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
940 Pattern::from_str(v)
941 .map(SerdePattern)
942 .map_err(serde::de::Error::custom)
943 }
944 }
945
946 deserializer.deserialize_str(Visitor)
947 }
948}
949
950#[cfg(feature = "schemars")]
951impl schemars::JsonSchema for SerdePattern {
952 fn schema_name() -> Cow<'static, str> {
953 Cow::Borrowed("SerdePattern")
954 }
955
956 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
957 <String as schemars::JsonSchema>::json_schema(generator)
958 }
959}
960
961impl Deref for SerdePattern {
962 type Target = Pattern;
963
964 fn deref(&self) -> &Self::Target {
965 &self.0
966 }
967}
968
969#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
970#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
971#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
972pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
973
974impl Sources {
975 pub fn iter(&self) -> impl Iterator<Item = &Source> {
981 self.0.iter()
982 }
983
984 pub fn is_empty(&self) -> bool {
986 self.0.is_empty()
987 }
988
989 pub fn len(&self) -> usize {
991 self.0.len()
992 }
993}
994
995impl FromIterator<Source> for Sources {
996 fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
997 Self(iter.into_iter().collect())
998 }
999}
1000
1001impl IntoIterator for Sources {
1002 type Item = Source;
1003 type IntoIter = std::vec::IntoIter<Source>;
1004
1005 fn into_iter(self) -> Self::IntoIter {
1006 self.0.into_iter()
1007 }
1008}
1009
1010#[derive(Debug, Clone, PartialEq, Eq)]
1011#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1012#[allow(clippy::large_enum_variant)]
1013enum SourcesWire {
1014 One(Source),
1015 Many(Vec<Source>),
1016}
1017
1018impl<'de> serde::de::Deserialize<'de> for SourcesWire {
1019 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1020 where
1021 D: Deserializer<'de>,
1022 {
1023 struct Visitor;
1024
1025 impl<'de> serde::de::Visitor<'de> for Visitor {
1026 type Value = SourcesWire;
1027
1028 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1029 formatter.write_str("a single source (as a map) or list of sources")
1030 }
1031
1032 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1033 where
1034 A: SeqAccess<'de>,
1035 {
1036 let sources = serde::de::Deserialize::deserialize(
1037 serde::de::value::SeqAccessDeserializer::new(seq),
1038 )?;
1039 Ok(SourcesWire::Many(sources))
1040 }
1041
1042 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1043 where
1044 M: serde::de::MapAccess<'de>,
1045 {
1046 let source = serde::de::Deserialize::deserialize(
1047 serde::de::value::MapAccessDeserializer::new(&mut map),
1048 )?;
1049 Ok(SourcesWire::One(source))
1050 }
1051 }
1052
1053 deserializer.deserialize_any(Visitor)
1054 }
1055}
1056
1057impl TryFrom<SourcesWire> for Sources {
1058 type Error = SourceError;
1059
1060 fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
1061 match wire {
1062 SourcesWire::One(source) => Ok(Self(vec![source])),
1063 SourcesWire::Many(sources) => {
1064 for (lhs, rhs) in sources.iter().zip(sources.iter().skip(1)) {
1065 if lhs.extra() != rhs.extra() {
1066 continue;
1067 }
1068 if lhs.group() != rhs.group() {
1069 continue;
1070 }
1071
1072 let lhs = lhs.marker();
1073 let rhs = rhs.marker();
1074 if !lhs.is_disjoint(rhs) {
1075 let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
1076 return Err(SourceError::MissingMarkers);
1077 };
1078
1079 let Some(right) = rhs.contents().map(|contents| contents.to_string())
1080 else {
1081 return Err(SourceError::MissingMarkers);
1082 };
1083
1084 let mut hint = lhs.negate();
1085 hint.and(rhs);
1086 let hint = hint
1087 .contents()
1088 .map(|contents| contents.to_string())
1089 .unwrap_or_else(|| "true".to_string());
1090
1091 return Err(SourceError::OverlappingMarkers(left, right, hint));
1092 }
1093 }
1094
1095 if sources.is_empty() {
1097 return Err(SourceError::EmptySources);
1098 }
1099
1100 Ok(Self(sources))
1101 }
1102 }
1103 }
1104}
1105
1106#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1108#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1109#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1110pub enum Source {
1111 Git {
1118 git: DisplaySafeUrl,
1120 subdirectory: Option<PortablePathBuf>,
1122 rev: Option<String>,
1124 tag: Option<String>,
1125 branch: Option<String>,
1126 lfs: Option<bool>,
1128 #[serde(
1129 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1130 serialize_with = "uv_pep508::marker::ser::serialize",
1131 default
1132 )]
1133 marker: MarkerTree,
1134 extra: Option<ExtraName>,
1135 group: Option<GroupName>,
1136 },
1137 Url {
1145 url: DisplaySafeUrl,
1146 subdirectory: Option<PortablePathBuf>,
1149 #[serde(
1150 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1151 serialize_with = "uv_pep508::marker::ser::serialize",
1152 default
1153 )]
1154 marker: MarkerTree,
1155 extra: Option<ExtraName>,
1156 group: Option<GroupName>,
1157 },
1158 Path {
1162 path: PortablePathBuf,
1163 editable: Option<bool>,
1165 package: Option<bool>,
1172 #[serde(
1173 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1174 serialize_with = "uv_pep508::marker::ser::serialize",
1175 default
1176 )]
1177 marker: MarkerTree,
1178 extra: Option<ExtraName>,
1179 group: Option<GroupName>,
1180 },
1181 Registry {
1183 index: IndexName,
1184 #[serde(
1185 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1186 serialize_with = "uv_pep508::marker::ser::serialize",
1187 default
1188 )]
1189 marker: MarkerTree,
1190 extra: Option<ExtraName>,
1191 group: Option<GroupName>,
1192 },
1193 Workspace {
1195 workspace: bool,
1198 editable: Option<bool>,
1200 #[serde(
1201 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1202 serialize_with = "uv_pep508::marker::ser::serialize",
1203 default
1204 )]
1205 marker: MarkerTree,
1206 extra: Option<ExtraName>,
1207 group: Option<GroupName>,
1208 },
1209}
1210
1211impl<'de> Deserialize<'de> for Source {
1214 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1215 where
1216 D: Deserializer<'de>,
1217 {
1218 #[derive(Deserialize, Debug, Clone)]
1219 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1220 struct CatchAll {
1221 git: Option<DisplaySafeUrl>,
1222 subdirectory: Option<PortablePathBuf>,
1223 rev: Option<String>,
1224 tag: Option<String>,
1225 branch: Option<String>,
1226 lfs: Option<bool>,
1227 url: Option<DisplaySafeUrl>,
1228 path: Option<PortablePathBuf>,
1229 editable: Option<bool>,
1230 package: Option<bool>,
1231 index: Option<IndexName>,
1232 workspace: Option<bool>,
1233 #[serde(
1234 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1235 serialize_with = "uv_pep508::marker::ser::serialize",
1236 default
1237 )]
1238 marker: MarkerTree,
1239 extra: Option<ExtraName>,
1240 group: Option<GroupName>,
1241 }
1242
1243 let CatchAll {
1245 git,
1246 subdirectory,
1247 rev,
1248 tag,
1249 branch,
1250 lfs,
1251 url,
1252 path,
1253 editable,
1254 package,
1255 index,
1256 workspace,
1257 marker,
1258 extra,
1259 group,
1260 } = CatchAll::deserialize(deserializer)?;
1261
1262 if extra.is_some() && group.is_some() {
1264 return Err(serde::de::Error::custom(
1265 "cannot specify both `extra` and `group`",
1266 ));
1267 }
1268
1269 if let Some(git) = git {
1271 if index.is_some() {
1272 return Err(serde::de::Error::custom(
1273 "cannot specify both `git` and `index`",
1274 ));
1275 }
1276 if workspace.is_some() {
1277 return Err(serde::de::Error::custom(
1278 "cannot specify both `git` and `workspace`",
1279 ));
1280 }
1281 if path.is_some() {
1282 return Err(serde::de::Error::custom(
1283 "cannot specify both `git` and `path`",
1284 ));
1285 }
1286 if url.is_some() {
1287 return Err(serde::de::Error::custom(
1288 "cannot specify both `git` and `url`",
1289 ));
1290 }
1291 if editable.is_some() {
1292 return Err(serde::de::Error::custom(
1293 "cannot specify both `git` and `editable`",
1294 ));
1295 }
1296 if package.is_some() {
1297 return Err(serde::de::Error::custom(
1298 "cannot specify both `git` and `package`",
1299 ));
1300 }
1301
1302 match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1304 (None, None, None) => {}
1305 (Some(_), None, None) => {}
1306 (None, Some(_), None) => {}
1307 (None, None, Some(_)) => {}
1308 _ => {
1309 return Err(serde::de::Error::custom(
1310 "expected at most one of `rev`, `tag`, or `branch`",
1311 ));
1312 }
1313 }
1314
1315 let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1317 DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1318 } else {
1319 git
1320 };
1321
1322 return Ok(Self::Git {
1323 git,
1324 subdirectory,
1325 rev,
1326 tag,
1327 branch,
1328 lfs,
1329 marker,
1330 extra,
1331 group,
1332 });
1333 }
1334
1335 if let Some(url) = url {
1337 if index.is_some() {
1338 return Err(serde::de::Error::custom(
1339 "cannot specify both `url` and `index`",
1340 ));
1341 }
1342 if workspace.is_some() {
1343 return Err(serde::de::Error::custom(
1344 "cannot specify both `url` and `workspace`",
1345 ));
1346 }
1347 if path.is_some() {
1348 return Err(serde::de::Error::custom(
1349 "cannot specify both `url` and `path`",
1350 ));
1351 }
1352 if git.is_some() {
1353 return Err(serde::de::Error::custom(
1354 "cannot specify both `url` and `git`",
1355 ));
1356 }
1357 if rev.is_some() {
1358 return Err(serde::de::Error::custom(
1359 "cannot specify both `url` and `rev`",
1360 ));
1361 }
1362 if tag.is_some() {
1363 return Err(serde::de::Error::custom(
1364 "cannot specify both `url` and `tag`",
1365 ));
1366 }
1367 if branch.is_some() {
1368 return Err(serde::de::Error::custom(
1369 "cannot specify both `url` and `branch`",
1370 ));
1371 }
1372 if editable.is_some() {
1373 return Err(serde::de::Error::custom(
1374 "cannot specify both `url` and `editable`",
1375 ));
1376 }
1377 if package.is_some() {
1378 return Err(serde::de::Error::custom(
1379 "cannot specify both `url` and `package`",
1380 ));
1381 }
1382
1383 return Ok(Self::Url {
1384 url,
1385 subdirectory,
1386 marker,
1387 extra,
1388 group,
1389 });
1390 }
1391
1392 if let Some(path) = path {
1394 if index.is_some() {
1395 return Err(serde::de::Error::custom(
1396 "cannot specify both `path` and `index`",
1397 ));
1398 }
1399 if workspace.is_some() {
1400 return Err(serde::de::Error::custom(
1401 "cannot specify both `path` and `workspace`",
1402 ));
1403 }
1404 if git.is_some() {
1405 return Err(serde::de::Error::custom(
1406 "cannot specify both `path` and `git`",
1407 ));
1408 }
1409 if url.is_some() {
1410 return Err(serde::de::Error::custom(
1411 "cannot specify both `path` and `url`",
1412 ));
1413 }
1414 if rev.is_some() {
1415 return Err(serde::de::Error::custom(
1416 "cannot specify both `path` and `rev`",
1417 ));
1418 }
1419 if tag.is_some() {
1420 return Err(serde::de::Error::custom(
1421 "cannot specify both `path` and `tag`",
1422 ));
1423 }
1424 if branch.is_some() {
1425 return Err(serde::de::Error::custom(
1426 "cannot specify both `path` and `branch`",
1427 ));
1428 }
1429
1430 if editable == Some(true) && package == Some(false) {
1432 return Err(serde::de::Error::custom(
1433 "cannot specify both `editable = true` and `package = false`",
1434 ));
1435 }
1436
1437 return Ok(Self::Path {
1438 path,
1439 editable,
1440 package,
1441 marker,
1442 extra,
1443 group,
1444 });
1445 }
1446
1447 if let Some(index) = index {
1449 if workspace.is_some() {
1450 return Err(serde::de::Error::custom(
1451 "cannot specify both `index` and `workspace`",
1452 ));
1453 }
1454 if git.is_some() {
1455 return Err(serde::de::Error::custom(
1456 "cannot specify both `index` and `git`",
1457 ));
1458 }
1459 if url.is_some() {
1460 return Err(serde::de::Error::custom(
1461 "cannot specify both `index` and `url`",
1462 ));
1463 }
1464 if path.is_some() {
1465 return Err(serde::de::Error::custom(
1466 "cannot specify both `index` and `path`",
1467 ));
1468 }
1469 if rev.is_some() {
1470 return Err(serde::de::Error::custom(
1471 "cannot specify both `index` and `rev`",
1472 ));
1473 }
1474 if tag.is_some() {
1475 return Err(serde::de::Error::custom(
1476 "cannot specify both `index` and `tag`",
1477 ));
1478 }
1479 if branch.is_some() {
1480 return Err(serde::de::Error::custom(
1481 "cannot specify both `index` and `branch`",
1482 ));
1483 }
1484 if editable.is_some() {
1485 return Err(serde::de::Error::custom(
1486 "cannot specify both `index` and `editable`",
1487 ));
1488 }
1489 if package.is_some() {
1490 return Err(serde::de::Error::custom(
1491 "cannot specify both `index` and `package`",
1492 ));
1493 }
1494
1495 return Ok(Self::Registry {
1496 index,
1497 marker,
1498 extra,
1499 group,
1500 });
1501 }
1502
1503 if let Some(workspace) = workspace {
1505 if index.is_some() {
1506 return Err(serde::de::Error::custom(
1507 "cannot specify both `workspace` and `index`",
1508 ));
1509 }
1510 if git.is_some() {
1511 return Err(serde::de::Error::custom(
1512 "cannot specify both `workspace` and `git`",
1513 ));
1514 }
1515 if url.is_some() {
1516 return Err(serde::de::Error::custom(
1517 "cannot specify both `workspace` and `url`",
1518 ));
1519 }
1520 if path.is_some() {
1521 return Err(serde::de::Error::custom(
1522 "cannot specify both `workspace` and `path`",
1523 ));
1524 }
1525 if rev.is_some() {
1526 return Err(serde::de::Error::custom(
1527 "cannot specify both `workspace` and `rev`",
1528 ));
1529 }
1530 if tag.is_some() {
1531 return Err(serde::de::Error::custom(
1532 "cannot specify both `workspace` and `tag`",
1533 ));
1534 }
1535 if branch.is_some() {
1536 return Err(serde::de::Error::custom(
1537 "cannot specify both `workspace` and `branch`",
1538 ));
1539 }
1540 if package.is_some() {
1541 return Err(serde::de::Error::custom(
1542 "cannot specify both `workspace` and `package`",
1543 ));
1544 }
1545
1546 return Ok(Self::Workspace {
1547 workspace,
1548 editable,
1549 marker,
1550 extra,
1551 group,
1552 });
1553 }
1554
1555 Err(serde::de::Error::custom(
1557 "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1558 ))
1559 }
1560}
1561
1562#[derive(Error, Debug)]
1563pub enum SourceError {
1564 #[error("Failed to resolve Git reference: `{0}`")]
1565 UnresolvedReference(String),
1566 #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1567 WorkspacePackageGit(String),
1568 #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1569 WorkspacePackageUrl(String),
1570 #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1571 WorkspacePackageFile(String),
1572 #[error(
1573 "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1574 )]
1575 UnusedRev(String, String),
1576 #[error(
1577 "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1578 )]
1579 UnusedTag(String, String),
1580 #[error(
1581 "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1582 )]
1583 UnusedBranch(String, String),
1584 #[error(
1585 "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1586 )]
1587 UnusedLfs(String),
1588 #[error(
1589 "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1590 )]
1591 UnusedEditable(String),
1592 #[error("Failed to resolve absolute path")]
1593 Absolute(#[from] std::io::Error),
1594 #[error("Path contains invalid characters: `{}`", _0.display())]
1595 NonUtf8Path(PathBuf),
1596 #[error("Source markers must be disjoint, but the following markers overlap: `{0}` and `{1}`.\n\n{hint}{colon} replace `{1}` with `{2}`.", hint = "hint".bold().cyan(), colon = ":".bold()
1597 )]
1598 OverlappingMarkers(String, String, String),
1599 #[error(
1600 "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1601 )]
1602 MissingMarkers,
1603 #[error("Must provide at least one source")]
1604 EmptySources,
1605}
1606
1607impl Source {
1608 pub fn from_requirement(
1609 name: &PackageName,
1610 source: RequirementSource,
1611 workspace: bool,
1612 editable: Option<bool>,
1613 index: Option<IndexName>,
1614 rev: Option<String>,
1615 tag: Option<String>,
1616 branch: Option<String>,
1617 lfs: GitLfsSetting,
1618 root: &Path,
1619 existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1620 ) -> Result<Option<Self>, SourceError> {
1621 if !matches!(source, RequirementSource::Git { .. })
1623 && (branch.is_some()
1624 || tag.is_some()
1625 || rev.is_some()
1626 || matches!(lfs, GitLfsSetting::Enabled { .. }))
1627 {
1628 if let Some(sources) = existing_sources {
1629 if let Some(package_sources) = sources.get(name) {
1630 for existing_source in package_sources.iter() {
1631 if let Self::Git {
1632 git,
1633 subdirectory,
1634 marker,
1635 extra,
1636 group,
1637 ..
1638 } = existing_source
1639 {
1640 return Ok(Some(Self::Git {
1641 git: git.clone(),
1642 subdirectory: subdirectory.clone(),
1643 rev,
1644 tag,
1645 branch,
1646 lfs: lfs.into(),
1647 marker: *marker,
1648 extra: extra.clone(),
1649 group: group.clone(),
1650 }));
1651 }
1652 }
1653 }
1654 }
1655 if let Some(rev) = rev {
1656 return Err(SourceError::UnusedRev(name.to_string(), rev));
1657 }
1658 if let Some(tag) = tag {
1659 return Err(SourceError::UnusedTag(name.to_string(), tag));
1660 }
1661 if let Some(branch) = branch {
1662 return Err(SourceError::UnusedBranch(name.to_string(), branch));
1663 }
1664 if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1665 return Err(SourceError::UnusedLfs(name.to_string()));
1666 }
1667 }
1668
1669 if !workspace {
1671 if !matches!(source, RequirementSource::Directory { .. }) {
1672 if editable == Some(true) {
1673 return Err(SourceError::UnusedEditable(name.to_string()));
1674 }
1675 }
1676 }
1677
1678 if workspace {
1680 return match source {
1681 RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1682 Ok(Some(Self::Workspace {
1683 workspace: true,
1684 editable,
1685 marker: MarkerTree::TRUE,
1686 extra: None,
1687 group: None,
1688 }))
1689 }
1690 RequirementSource::Url { .. } => {
1691 Err(SourceError::WorkspacePackageUrl(name.to_string()))
1692 }
1693 RequirementSource::Git { .. } => {
1694 Err(SourceError::WorkspacePackageGit(name.to_string()))
1695 }
1696 RequirementSource::Path { .. } => {
1697 Err(SourceError::WorkspacePackageFile(name.to_string()))
1698 }
1699 };
1700 }
1701
1702 let source = match source {
1703 RequirementSource::Registry { index: Some(_), .. } => {
1704 return Ok(None);
1705 }
1706 RequirementSource::Registry { index: None, .. } => {
1707 if let Some(index) = index {
1708 Self::Registry {
1709 index,
1710 marker: MarkerTree::TRUE,
1711 extra: None,
1712 group: None,
1713 }
1714 } else {
1715 return Ok(None);
1716 }
1717 }
1718 RequirementSource::Path { install_path, .. } => Self::Path {
1719 editable: None,
1720 package: None,
1721 path: PortablePathBuf::from(
1722 relative_to(&install_path, root)
1723 .or_else(|_| std::path::absolute(&install_path))
1724 .map_err(SourceError::Absolute)?
1725 .into_boxed_path(),
1726 ),
1727 marker: MarkerTree::TRUE,
1728 extra: None,
1729 group: None,
1730 },
1731 RequirementSource::Directory {
1732 install_path,
1733 editable: is_editable,
1734 ..
1735 } => Self::Path {
1736 editable: editable.or(is_editable),
1737 package: None,
1738 path: PortablePathBuf::from(
1739 relative_to(&install_path, root)
1740 .or_else(|_| std::path::absolute(&install_path))
1741 .map_err(SourceError::Absolute)?
1742 .into_boxed_path(),
1743 ),
1744 marker: MarkerTree::TRUE,
1745 extra: None,
1746 group: None,
1747 },
1748 RequirementSource::Url {
1749 location,
1750 subdirectory,
1751 ..
1752 } => Self::Url {
1753 url: location,
1754 subdirectory: subdirectory.map(PortablePathBuf::from),
1755 marker: MarkerTree::TRUE,
1756 extra: None,
1757 group: None,
1758 },
1759 RequirementSource::Git {
1760 git, subdirectory, ..
1761 } => {
1762 if rev.is_none() && tag.is_none() && branch.is_none() {
1763 let rev = match git.reference() {
1764 GitReference::Branch(rev) => Some(rev),
1765 GitReference::Tag(rev) => Some(rev),
1766 GitReference::BranchOrTag(rev) => Some(rev),
1767 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1768 GitReference::NamedRef(rev) => Some(rev),
1769 GitReference::DefaultBranch => None,
1770 };
1771 Self::Git {
1772 rev: rev.cloned(),
1773 tag,
1774 branch,
1775 lfs: lfs.into(),
1776 git: git.repository().clone(),
1777 subdirectory: subdirectory.map(PortablePathBuf::from),
1778 marker: MarkerTree::TRUE,
1779 extra: None,
1780 group: None,
1781 }
1782 } else {
1783 Self::Git {
1784 rev,
1785 tag,
1786 branch,
1787 lfs: lfs.into(),
1788 git: git.repository().clone(),
1789 subdirectory: subdirectory.map(PortablePathBuf::from),
1790 marker: MarkerTree::TRUE,
1791 extra: None,
1792 group: None,
1793 }
1794 }
1795 }
1796 };
1797
1798 Ok(Some(source))
1799 }
1800
1801 pub fn marker(&self) -> MarkerTree {
1803 match self {
1804 Self::Git { marker, .. } => *marker,
1805 Self::Url { marker, .. } => *marker,
1806 Self::Path { marker, .. } => *marker,
1807 Self::Registry { marker, .. } => *marker,
1808 Self::Workspace { marker, .. } => *marker,
1809 }
1810 }
1811
1812 pub fn extra(&self) -> Option<&ExtraName> {
1814 match self {
1815 Self::Git { extra, .. } => extra.as_ref(),
1816 Self::Url { extra, .. } => extra.as_ref(),
1817 Self::Path { extra, .. } => extra.as_ref(),
1818 Self::Registry { extra, .. } => extra.as_ref(),
1819 Self::Workspace { extra, .. } => extra.as_ref(),
1820 }
1821 }
1822
1823 pub fn group(&self) -> Option<&GroupName> {
1825 match self {
1826 Self::Git { group, .. } => group.as_ref(),
1827 Self::Url { group, .. } => group.as_ref(),
1828 Self::Path { group, .. } => group.as_ref(),
1829 Self::Registry { group, .. } => group.as_ref(),
1830 Self::Workspace { group, .. } => group.as_ref(),
1831 }
1832 }
1833}
1834
1835#[derive(Debug, Clone, PartialEq, Eq)]
1837pub enum DependencyType {
1838 Production,
1840 Dev,
1842 Optional(ExtraName),
1844 Group(GroupName),
1846}
1847
1848#[derive(Debug, Clone, PartialEq, Eq)]
1849#[cfg_attr(test, derive(Serialize))]
1850pub struct BuildBackendSettingsSchema;
1851
1852impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1853 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1854 where
1855 D: Deserializer<'de>,
1856 {
1857 Ok(Self)
1858 }
1859}
1860
1861#[cfg(feature = "schemars")]
1862impl schemars::JsonSchema for BuildBackendSettingsSchema {
1863 fn schema_name() -> Cow<'static, str> {
1864 BuildBackendSettings::schema_name()
1865 }
1866
1867 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1868 BuildBackendSettings::json_schema(generator)
1869 }
1870}
1871
1872impl OptionsMetadata for BuildBackendSettingsSchema {
1873 fn record(visit: &mut dyn Visit) {
1874 BuildBackendSettings::record(visit);
1875 }
1876
1877 fn documentation() -> Option<&'static str> {
1878 BuildBackendSettings::documentation()
1879 }
1880
1881 fn metadata() -> OptionSet
1882 where
1883 Self: Sized + 'static,
1884 {
1885 BuildBackendSettings::metadata()
1886 }
1887}