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;
38use uv_warnings::warn_user_once;
39
40#[derive(Error, Debug)]
41pub enum PyprojectTomlError {
42 #[error(transparent)]
43 TomlSyntax(#[from] toml_edit::TomlError),
44 #[error(transparent)]
45 TomlSchema(#[from] toml_edit::de::Error),
46 #[error(
47 "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
48 )]
49 MissingName,
50 #[error(
51 "`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list"
52 )]
53 MissingVersion,
54}
55
56fn deserialize_unique_map<'de, D, K, V, F>(
58 deserializer: D,
59 error_msg: F,
60) -> Result<BTreeMap<K, V>, D::Error>
61where
62 D: Deserializer<'de>,
63 K: Deserialize<'de> + Ord + std::fmt::Display,
64 V: Deserialize<'de>,
65 F: FnOnce(&K) -> String,
66{
67 struct Visitor<K, V, F>(F, std::marker::PhantomData<(K, V)>);
68
69 impl<'de, K, V, F> serde::de::Visitor<'de> for Visitor<K, V, F>
70 where
71 K: Deserialize<'de> + Ord + std::fmt::Display,
72 V: Deserialize<'de>,
73 F: FnOnce(&K) -> String,
74 {
75 type Value = BTreeMap<K, V>;
76
77 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
78 formatter.write_str("a map with unique keys")
79 }
80
81 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
82 where
83 M: serde::de::MapAccess<'de>,
84 {
85 use std::collections::btree_map::Entry;
86
87 let mut map = BTreeMap::new();
88 while let Some((key, value)) = access.next_entry::<K, V>()? {
89 match map.entry(key) {
90 Entry::Occupied(entry) => {
91 return Err(serde::de::Error::custom((self.0)(entry.key())));
92 }
93 Entry::Vacant(entry) => {
94 entry.insert(value);
95 }
96 }
97 }
98 Ok(map)
99 }
100 }
101
102 deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData))
103}
104
105#[derive(Deserialize, Debug, Clone)]
107#[cfg_attr(test, derive(Serialize))]
108#[serde(rename_all = "kebab-case")]
109pub struct PyProjectToml {
110 pub project: Option<Project>,
112 pub tool: Option<Tool>,
114 pub dependency_groups: Option<DependencyGroups>,
116 #[serde(skip)]
118 pub raw: String,
119
120 #[serde(default, skip_serializing)]
122 pub build_system: Option<serde::de::IgnoredAny>,
123}
124
125impl PyProjectToml {
126 pub fn from_string(raw: String) -> Result<Self, PyprojectTomlError> {
128 let pyproject =
129 toml_edit::Document::from_str(&raw).map_err(PyprojectTomlError::TomlSyntax)?;
130 let pyproject = Self::deserialize(pyproject.into_deserializer())
131 .map_err(PyprojectTomlError::TomlSchema)?;
132 Ok(Self { raw, ..pyproject })
133 }
134
135 pub fn is_package(&self, require_build_system: bool) -> bool {
138 if let Some(is_package) = self.tool_uv_package() {
140 return is_package;
141 }
142
143 self.build_system.is_some() || !require_build_system
145 }
146
147 fn tool_uv_package(&self) -> Option<bool> {
149 self.tool
150 .as_ref()
151 .and_then(|tool| tool.uv.as_ref())
152 .and_then(|uv| uv.package)
153 }
154
155 pub fn is_dynamic(&self) -> bool {
157 self.project
158 .as_ref()
159 .is_some_and(|project| project.version.is_none())
160 }
161
162 pub fn has_scripts(&self) -> bool {
164 if let Some(ref project) = self.project {
165 project.gui_scripts.is_some() || project.scripts.is_some()
166 } else {
167 false
168 }
169 }
170
171 pub fn conflicts(&self) -> Conflicts {
173 let empty = Conflicts::empty();
174 let Some(project) = self.project.as_ref() else {
175 return empty;
176 };
177 let Some(tool) = self.tool.as_ref() else {
178 return empty;
179 };
180 let Some(tooluv) = tool.uv.as_ref() else {
181 return empty;
182 };
183 let Some(conflicting) = tooluv.conflicts.as_ref() else {
184 return empty;
185 };
186 conflicting.to_conflicts_with_package_name(&project.name)
187 }
188}
189
190impl PartialEq for PyProjectToml {
192 fn eq(&self, other: &Self) -> bool {
193 self.project.eq(&other.project) && self.tool.eq(&other.tool)
194 }
195}
196
197impl Eq for PyProjectToml {}
198
199impl AsRef<[u8]> for PyProjectToml {
200 fn as_ref(&self) -> &[u8] {
201 self.raw.as_bytes()
202 }
203}
204
205#[derive(Deserialize, Debug, Clone, PartialEq)]
209#[cfg_attr(test, derive(Serialize))]
210#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
211pub struct Project {
212 pub name: PackageName,
214 pub version: Option<Version>,
216 pub requires_python: Option<VersionSpecifiers>,
218 pub dependencies: Option<Vec<String>>,
220 pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
222
223 #[serde(default, skip_serializing)]
225 pub(crate) gui_scripts: Option<serde::de::IgnoredAny>,
226 #[serde(default, skip_serializing)]
228 pub(crate) scripts: Option<serde::de::IgnoredAny>,
229}
230
231#[derive(Deserialize, Debug)]
232#[serde(rename_all = "kebab-case")]
233struct ProjectWire {
234 name: Option<PackageName>,
235 version: Option<Version>,
236 dynamic: Option<Vec<String>>,
237 requires_python: Option<VersionSpecifiers>,
238 dependencies: Option<Vec<String>>,
239 optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
240 gui_scripts: Option<serde::de::IgnoredAny>,
241 scripts: Option<serde::de::IgnoredAny>,
242}
243
244impl TryFrom<ProjectWire> for Project {
245 type Error = PyprojectTomlError;
246
247 fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
248 let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
250
251 if value.version.is_none()
253 && !value
254 .dynamic
255 .as_ref()
256 .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
257 {
258 return Err(PyprojectTomlError::MissingVersion);
259 }
260
261 Ok(Self {
262 name,
263 version: value.version,
264 requires_python: value.requires_python,
265 dependencies: value.dependencies,
266 optional_dependencies: value.optional_dependencies,
267 gui_scripts: value.gui_scripts,
268 scripts: value.scripts,
269 })
270 }
271}
272
273#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
274#[cfg_attr(test, derive(Serialize))]
275#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
276pub struct Tool {
277 pub uv: Option<ToolUv>,
278}
279
280fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
286where
287 D: Deserializer<'de>,
288{
289 let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
290 if let Some(indexes) = indexes.as_ref() {
291 let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
292 let mut seen_default = false;
293 for index in indexes {
294 if let Some(name) = index.name.as_ref() {
295 if !seen_names.insert(name) {
296 return Err(serde::de::Error::custom(format!(
297 "duplicate index name `{name}`"
298 )));
299 }
300 }
301 if index.default {
302 if seen_default {
303 warn_user_once!(
304 "Found multiple indexes with `default = true`; \
305 only one index may be marked as default. \
306 This will become an error in the future.",
307 );
308 }
309 seen_default = true;
310 }
311 }
312 }
313 Ok(indexes)
314}
315
316#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
319#[cfg_attr(test, derive(Serialize))]
320#[serde(rename_all = "kebab-case")]
321#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
322pub struct ToolUv {
323 #[option(
331 default = "{}",
332 value_type = "dict",
333 example = r#"
334 [tool.uv.sources]
335 httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
336 pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
337 pydantic = { path = "/path/to/pydantic", editable = true }
338 "#
339 )]
340 pub sources: Option<ToolUvSources>,
341
342 #[option(
370 default = "[]",
371 value_type = "dict",
372 example = r#"
373 [[tool.uv.index]]
374 name = "pytorch"
375 url = "https://download.pytorch.org/whl/cu121"
376 "#
377 )]
378 #[serde(deserialize_with = "deserialize_index_vec", default)]
379 pub index: Option<Vec<Index>>,
380
381 #[option_group]
383 pub workspace: Option<ToolUvWorkspace>,
384
385 #[option(
388 default = r#"true"#,
389 value_type = "bool",
390 example = r#"
391 managed = false
392 "#
393 )]
394 pub managed: Option<bool>,
395
396 #[option(
407 default = r#"true"#,
408 value_type = "bool",
409 example = r#"
410 package = false
411 "#
412 )]
413 pub package: Option<bool>,
414
415 #[option(
419 default = r#"["dev"]"#,
420 value_type = r#"str | list[str]"#,
421 example = r#"
422 default-groups = ["docs"]
423 "#
424 )]
425 pub default_groups: Option<DefaultGroups>,
426
427 #[option(
436 default = "[]",
437 value_type = "dict",
438 example = r#"
439 [tool.uv.dependency-groups]
440 my-group = {requires-python = ">=3.12"}
441 "#
442 )]
443 pub dependency_groups: Option<ToolUvDependencyGroups>,
444
445 #[cfg_attr(
455 feature = "schemars",
456 schemars(
457 with = "Option<Vec<String>>",
458 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
459 )
460 )]
461 #[option(
462 default = "[]",
463 value_type = "list[str]",
464 example = r#"
465 dev-dependencies = ["ruff==0.5.0"]
466 "#
467 )]
468 pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
469
470 #[cfg_attr(
489 feature = "schemars",
490 schemars(
491 with = "Option<Vec<String>>",
492 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
493 )
494 )]
495 #[option(
496 default = "[]",
497 value_type = "list[str]",
498 example = r#"
499 # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
500 # a different version.
501 override-dependencies = ["werkzeug==2.3.0"]
502 "#
503 )]
504 pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
505
506 #[cfg_attr(
521 feature = "schemars",
522 schemars(
523 with = "Option<Vec<String>>",
524 description = "Package names to exclude, e.g., `werkzeug`, `numpy`."
525 )
526 )]
527 #[option(
528 default = "[]",
529 value_type = "list[str]",
530 example = r#"
531 # Exclude Werkzeug from being installed, even if transitive dependencies request it.
532 exclude-dependencies = ["werkzeug"]
533 "#
534 )]
535 pub exclude_dependencies: Option<Vec<PackageName>>,
536
537 #[cfg_attr(
551 feature = "schemars",
552 schemars(
553 with = "Option<Vec<String>>",
554 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
555 )
556 )]
557 #[option(
558 default = "[]",
559 value_type = "list[str]",
560 example = r#"
561 # Ensure that the grpcio version is always less than 1.65, if it's requested by a
562 # direct or transitive dependency.
563 constraint-dependencies = ["grpcio<1.65"]
564 "#
565 )]
566 pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
567
568 #[cfg_attr(
582 feature = "schemars",
583 schemars(
584 with = "Option<Vec<String>>",
585 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
586 )
587 )]
588 #[option(
589 default = "[]",
590 value_type = "list[str]",
591 example = r#"
592 # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
593 # on setuptools.
594 build-constraint-dependencies = ["setuptools==60.0.0"]
595 "#
596 )]
597 pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
598
599 #[cfg_attr(
608 feature = "schemars",
609 schemars(
610 with = "Option<Vec<String>>",
611 description = "A list of environment markers, e.g., `python_version >= '3.6'`."
612 )
613 )]
614 #[option(
615 default = "[]",
616 value_type = "str | list[str]",
617 example = r#"
618 # Resolve for macOS, but not for Linux or Windows.
619 environments = ["sys_platform == 'darwin'"]
620 "#
621 )]
622 pub environments: Option<SupportedEnvironments>,
623
624 #[cfg_attr(
644 feature = "schemars",
645 schemars(
646 with = "Option<Vec<String>>",
647 description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
648 )
649 )]
650 #[option(
651 default = "[]",
652 value_type = "str | list[str]",
653 example = r#"
654 # Require that the package is available for macOS ARM and x86 (Intel).
655 required-environments = [
656 "sys_platform == 'darwin' and platform_machine == 'arm64'",
657 "sys_platform == 'darwin' and platform_machine == 'x86_64'",
658 ]
659 "#
660 )]
661 pub required_environments: Option<SupportedEnvironments>,
662
663 #[cfg_attr(
678 feature = "schemars",
679 schemars(description = "A list of sets of conflicting groups or extras.")
680 )]
681 #[option(
682 default = r#"[]"#,
683 value_type = "list[list[dict]]",
684 example = r#"
685 # Require that `package[extra1]` and `package[extra2]` are resolved
686 # in different forks so that they cannot conflict with one another.
687 conflicts = [
688 [
689 { extra = "extra1" },
690 { extra = "extra2" },
691 ]
692 ]
693
694 # Require that the dependency groups `group1` and `group2`
695 # are resolved in different forks so that they cannot conflict
696 # with one another.
697 conflicts = [
698 [
699 { group = "group1" },
700 { group = "group2" },
701 ]
702 ]
703 "#
704 )]
705 pub conflicts: Option<SchemaConflicts>,
706
707 #[option_group]
714 pub build_backend: Option<BuildBackendSettingsSchema>,
715}
716
717#[derive(Default, Debug, Clone, PartialEq, Eq)]
718#[cfg_attr(test, derive(Serialize))]
719#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
720pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
721
722impl ToolUvSources {
723 pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
725 &self.0
726 }
727
728 #[must_use]
730 pub fn into_inner(self) -> BTreeMap<PackageName, Sources> {
731 self.0
732 }
733}
734
735impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
737 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
738 where
739 D: Deserializer<'de>,
740 {
741 deserialize_unique_map(deserializer, |key: &PackageName| {
742 format!("duplicate sources for package `{key}`")
743 })
744 .map(ToolUvSources)
745 }
746}
747
748#[derive(Default, Debug, Clone, PartialEq, Eq)]
749#[cfg_attr(test, derive(Serialize))]
750#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
751pub struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
752
753impl ToolUvDependencyGroups {
754 pub fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
756 &self.0
757 }
758
759 #[must_use]
761 pub fn into_inner(self) -> BTreeMap<GroupName, DependencyGroupSettings> {
762 self.0
763 }
764}
765
766impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
768 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
769 where
770 D: Deserializer<'de>,
771 {
772 deserialize_unique_map(deserializer, |key: &GroupName| {
773 format!("duplicate settings for dependency group `{key}`")
774 })
775 .map(ToolUvDependencyGroups)
776 }
777}
778
779#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
780#[cfg_attr(test, derive(Serialize))]
781#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
782#[serde(rename_all = "kebab-case")]
783pub struct DependencyGroupSettings {
784 #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
786 pub requires_python: Option<VersionSpecifiers>,
787}
788
789#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
790#[serde(untagged, rename_all = "kebab-case")]
791#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
792pub enum ExtraBuildDependencyWire {
793 Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
794 #[serde(rename_all = "kebab-case")]
795 Annotated {
796 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
797 match_runtime: bool,
798 },
799}
800
801#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
802#[serde(
803 deny_unknown_fields,
804 from = "ExtraBuildDependencyWire",
805 into = "ExtraBuildDependencyWire"
806)]
807#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
808pub struct ExtraBuildDependency {
809 pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
810 pub match_runtime: bool,
811}
812
813impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
814 fn from(value: ExtraBuildDependency) -> Self {
815 value.requirement
816 }
817}
818
819impl From<ExtraBuildDependencyWire> for ExtraBuildDependency {
820 fn from(wire: ExtraBuildDependencyWire) -> Self {
821 match wire {
822 ExtraBuildDependencyWire::Unannotated(requirement) => Self {
823 requirement,
824 match_runtime: false,
825 },
826 ExtraBuildDependencyWire::Annotated {
827 requirement,
828 match_runtime,
829 } => Self {
830 requirement,
831 match_runtime,
832 },
833 }
834 }
835}
836
837impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
838 fn from(item: ExtraBuildDependency) -> Self {
839 Self::Annotated {
840 requirement: item.requirement,
841 match_runtime: item.match_runtime,
842 }
843 }
844}
845
846#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
847#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
848pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
849
850impl std::ops::Deref for ExtraBuildDependencies {
851 type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
852
853 fn deref(&self) -> &Self::Target {
854 &self.0
855 }
856}
857
858impl std::ops::DerefMut for ExtraBuildDependencies {
859 fn deref_mut(&mut self) -> &mut Self::Target {
860 &mut self.0
861 }
862}
863
864impl IntoIterator for ExtraBuildDependencies {
865 type Item = (PackageName, Vec<ExtraBuildDependency>);
866 type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
867
868 fn into_iter(self) -> Self::IntoIter {
869 self.0.into_iter()
870 }
871}
872
873impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
874 fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
875 iter: T,
876 ) -> Self {
877 Self(iter.into_iter().collect())
878 }
879}
880
881impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
883 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
884 where
885 D: Deserializer<'de>,
886 {
887 deserialize_unique_map(deserializer, |key: &PackageName| {
888 format!("duplicate extra-build-dependencies for `{key}`")
889 })
890 .map(ExtraBuildDependencies)
891 }
892}
893
894#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
895#[cfg_attr(test, derive(Serialize))]
896#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
897#[serde(rename_all = "kebab-case", deny_unknown_fields)]
898pub struct ToolUvWorkspace {
899 #[option(
905 default = "[]",
906 value_type = "list[str]",
907 example = r#"
908 members = ["member1", "path/to/member2", "libs/*"]
909 "#
910 )]
911 pub members: Option<Vec<SerdePattern>>,
912 #[option(
919 default = "[]",
920 value_type = "list[str]",
921 example = r#"
922 exclude = ["member1", "path/to/member2", "libs/*"]
923 "#
924 )]
925 pub exclude: Option<Vec<SerdePattern>>,
926}
927
928#[derive(Debug, Clone, PartialEq, Eq)]
930pub struct SerdePattern(Pattern);
931
932impl serde::ser::Serialize for SerdePattern {
933 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
934 where
935 S: serde::ser::Serializer,
936 {
937 self.0.as_str().serialize(serializer)
938 }
939}
940
941impl<'de> serde::Deserialize<'de> for SerdePattern {
942 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
943 struct Visitor;
944
945 impl serde::de::Visitor<'_> for Visitor {
946 type Value = SerdePattern;
947
948 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
949 f.write_str("a string")
950 }
951
952 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
953 Pattern::from_str(v)
954 .map(SerdePattern)
955 .map_err(serde::de::Error::custom)
956 }
957 }
958
959 deserializer.deserialize_str(Visitor)
960 }
961}
962
963#[cfg(feature = "schemars")]
964impl schemars::JsonSchema for SerdePattern {
965 fn schema_name() -> Cow<'static, str> {
966 Cow::Borrowed("SerdePattern")
967 }
968
969 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
970 <String as schemars::JsonSchema>::json_schema(generator)
971 }
972}
973
974impl Deref for SerdePattern {
975 type Target = Pattern;
976
977 fn deref(&self) -> &Self::Target {
978 &self.0
979 }
980}
981
982#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
983#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
984#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
985pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
986
987impl Sources {
988 pub fn iter(&self) -> impl Iterator<Item = &Source> {
994 self.0.iter()
995 }
996
997 pub fn is_empty(&self) -> bool {
999 self.0.is_empty()
1000 }
1001
1002 pub fn len(&self) -> usize {
1004 self.0.len()
1005 }
1006}
1007
1008impl FromIterator<Source> for Sources {
1009 fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
1010 Self(iter.into_iter().collect())
1011 }
1012}
1013
1014impl IntoIterator for Sources {
1015 type Item = Source;
1016 type IntoIter = std::vec::IntoIter<Source>;
1017
1018 fn into_iter(self) -> Self::IntoIter {
1019 self.0.into_iter()
1020 }
1021}
1022
1023#[derive(Debug, Clone, PartialEq, Eq)]
1024#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1025enum SourcesWire {
1026 One(Source),
1027 Many(Vec<Source>),
1028}
1029
1030impl<'de> serde::de::Deserialize<'de> for SourcesWire {
1031 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1032 where
1033 D: Deserializer<'de>,
1034 {
1035 struct Visitor;
1036
1037 impl<'de> serde::de::Visitor<'de> for Visitor {
1038 type Value = SourcesWire;
1039
1040 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1041 formatter.write_str("a single source (as a map) or list of sources")
1042 }
1043
1044 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1045 where
1046 A: SeqAccess<'de>,
1047 {
1048 let sources = serde::de::Deserialize::deserialize(
1049 serde::de::value::SeqAccessDeserializer::new(seq),
1050 )?;
1051 Ok(SourcesWire::Many(sources))
1052 }
1053
1054 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1055 where
1056 M: serde::de::MapAccess<'de>,
1057 {
1058 let source = serde::de::Deserialize::deserialize(
1059 serde::de::value::MapAccessDeserializer::new(&mut map),
1060 )?;
1061 Ok(SourcesWire::One(source))
1062 }
1063 }
1064
1065 deserializer.deserialize_any(Visitor)
1066 }
1067}
1068
1069impl TryFrom<SourcesWire> for Sources {
1070 type Error = SourceError;
1071
1072 fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
1073 match wire {
1074 SourcesWire::One(source) => Ok(Self(vec![source])),
1075 SourcesWire::Many(sources) => {
1076 for (lhs, rhs) in sources.iter().zip(sources.iter().skip(1)) {
1077 if lhs.extra() != rhs.extra() {
1078 continue;
1079 }
1080 if lhs.group() != rhs.group() {
1081 continue;
1082 }
1083
1084 let lhs = lhs.marker();
1085 let rhs = rhs.marker();
1086 if !lhs.is_disjoint(rhs) {
1087 let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
1088 return Err(SourceError::MissingMarkers);
1089 };
1090
1091 let Some(right) = rhs.contents().map(|contents| contents.to_string())
1092 else {
1093 return Err(SourceError::MissingMarkers);
1094 };
1095
1096 let mut hint = lhs.negate();
1097 hint.and(rhs);
1098 let hint = hint
1099 .contents()
1100 .map(|contents| contents.to_string())
1101 .unwrap_or_else(|| "true".to_string());
1102
1103 return Err(SourceError::OverlappingMarkers(left, right, hint));
1104 }
1105 }
1106
1107 if sources.is_empty() {
1109 return Err(SourceError::EmptySources);
1110 }
1111
1112 Ok(Self(sources))
1113 }
1114 }
1115 }
1116}
1117
1118#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1120#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1121#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1122pub enum Source {
1123 Git {
1130 git: DisplaySafeUrl,
1132 subdirectory: Option<PortablePathBuf>,
1134 rev: Option<String>,
1136 tag: Option<String>,
1137 branch: Option<String>,
1138 lfs: Option<bool>,
1140 #[serde(
1141 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1142 serialize_with = "uv_pep508::marker::ser::serialize",
1143 default
1144 )]
1145 marker: MarkerTree,
1146 extra: Option<ExtraName>,
1147 group: Option<GroupName>,
1148 },
1149 Url {
1157 url: DisplaySafeUrl,
1158 subdirectory: Option<PortablePathBuf>,
1161 #[serde(
1162 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1163 serialize_with = "uv_pep508::marker::ser::serialize",
1164 default
1165 )]
1166 marker: MarkerTree,
1167 extra: Option<ExtraName>,
1168 group: Option<GroupName>,
1169 },
1170 Path {
1174 path: PortablePathBuf,
1175 editable: Option<bool>,
1177 package: Option<bool>,
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 Registry {
1195 index: IndexName,
1196 #[serde(
1197 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1198 serialize_with = "uv_pep508::marker::ser::serialize",
1199 default
1200 )]
1201 marker: MarkerTree,
1202 extra: Option<ExtraName>,
1203 group: Option<GroupName>,
1204 },
1205 Workspace {
1207 workspace: bool,
1210 editable: Option<bool>,
1212 #[serde(
1213 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1214 serialize_with = "uv_pep508::marker::ser::serialize",
1215 default
1216 )]
1217 marker: MarkerTree,
1218 extra: Option<ExtraName>,
1219 group: Option<GroupName>,
1220 },
1221}
1222
1223impl<'de> Deserialize<'de> for Source {
1226 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1227 where
1228 D: Deserializer<'de>,
1229 {
1230 #[derive(Deserialize, Debug, Clone)]
1231 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1232 struct CatchAll {
1233 git: Option<DisplaySafeUrl>,
1234 subdirectory: Option<PortablePathBuf>,
1235 rev: Option<String>,
1236 tag: Option<String>,
1237 branch: Option<String>,
1238 lfs: Option<bool>,
1239 url: Option<DisplaySafeUrl>,
1240 path: Option<PortablePathBuf>,
1241 editable: Option<bool>,
1242 package: Option<bool>,
1243 index: Option<IndexName>,
1244 workspace: Option<bool>,
1245 #[serde(
1246 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1247 serialize_with = "uv_pep508::marker::ser::serialize",
1248 default
1249 )]
1250 marker: MarkerTree,
1251 extra: Option<ExtraName>,
1252 group: Option<GroupName>,
1253 }
1254
1255 let CatchAll {
1257 git,
1258 subdirectory,
1259 rev,
1260 tag,
1261 branch,
1262 lfs,
1263 url,
1264 path,
1265 editable,
1266 package,
1267 index,
1268 workspace,
1269 marker,
1270 extra,
1271 group,
1272 } = CatchAll::deserialize(deserializer)?;
1273
1274 if extra.is_some() && group.is_some() {
1276 return Err(serde::de::Error::custom(
1277 "cannot specify both `extra` and `group`",
1278 ));
1279 }
1280
1281 if let Some(git) = git {
1283 if index.is_some() {
1284 return Err(serde::de::Error::custom(
1285 "cannot specify both `git` and `index`",
1286 ));
1287 }
1288 if workspace.is_some() {
1289 return Err(serde::de::Error::custom(
1290 "cannot specify both `git` and `workspace`",
1291 ));
1292 }
1293 if path.is_some() {
1294 return Err(serde::de::Error::custom(
1295 "cannot specify both `git` and `path`",
1296 ));
1297 }
1298 if url.is_some() {
1299 return Err(serde::de::Error::custom(
1300 "cannot specify both `git` and `url`",
1301 ));
1302 }
1303 if editable.is_some() {
1304 return Err(serde::de::Error::custom(
1305 "cannot specify both `git` and `editable`",
1306 ));
1307 }
1308 if package.is_some() {
1309 return Err(serde::de::Error::custom(
1310 "cannot specify both `git` and `package`",
1311 ));
1312 }
1313
1314 match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1316 (None, None, None) => {}
1317 (Some(_), None, None) => {}
1318 (None, Some(_), None) => {}
1319 (None, None, Some(_)) => {}
1320 _ => {
1321 return Err(serde::de::Error::custom(
1322 "expected at most one of `rev`, `tag`, or `branch`",
1323 ));
1324 }
1325 }
1326
1327 let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1329 DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1330 } else {
1331 git
1332 };
1333
1334 return Ok(Self::Git {
1335 git,
1336 subdirectory,
1337 rev,
1338 tag,
1339 branch,
1340 lfs,
1341 marker,
1342 extra,
1343 group,
1344 });
1345 }
1346
1347 if let Some(url) = url {
1349 if index.is_some() {
1350 return Err(serde::de::Error::custom(
1351 "cannot specify both `url` and `index`",
1352 ));
1353 }
1354 if workspace.is_some() {
1355 return Err(serde::de::Error::custom(
1356 "cannot specify both `url` and `workspace`",
1357 ));
1358 }
1359 if path.is_some() {
1360 return Err(serde::de::Error::custom(
1361 "cannot specify both `url` and `path`",
1362 ));
1363 }
1364 if git.is_some() {
1365 return Err(serde::de::Error::custom(
1366 "cannot specify both `url` and `git`",
1367 ));
1368 }
1369 if rev.is_some() {
1370 return Err(serde::de::Error::custom(
1371 "cannot specify both `url` and `rev`",
1372 ));
1373 }
1374 if tag.is_some() {
1375 return Err(serde::de::Error::custom(
1376 "cannot specify both `url` and `tag`",
1377 ));
1378 }
1379 if branch.is_some() {
1380 return Err(serde::de::Error::custom(
1381 "cannot specify both `url` and `branch`",
1382 ));
1383 }
1384 if editable.is_some() {
1385 return Err(serde::de::Error::custom(
1386 "cannot specify both `url` and `editable`",
1387 ));
1388 }
1389 if package.is_some() {
1390 return Err(serde::de::Error::custom(
1391 "cannot specify both `url` and `package`",
1392 ));
1393 }
1394
1395 return Ok(Self::Url {
1396 url,
1397 subdirectory,
1398 marker,
1399 extra,
1400 group,
1401 });
1402 }
1403
1404 if let Some(path) = path {
1406 if index.is_some() {
1407 return Err(serde::de::Error::custom(
1408 "cannot specify both `path` and `index`",
1409 ));
1410 }
1411 if workspace.is_some() {
1412 return Err(serde::de::Error::custom(
1413 "cannot specify both `path` and `workspace`",
1414 ));
1415 }
1416 if git.is_some() {
1417 return Err(serde::de::Error::custom(
1418 "cannot specify both `path` and `git`",
1419 ));
1420 }
1421 if url.is_some() {
1422 return Err(serde::de::Error::custom(
1423 "cannot specify both `path` and `url`",
1424 ));
1425 }
1426 if rev.is_some() {
1427 return Err(serde::de::Error::custom(
1428 "cannot specify both `path` and `rev`",
1429 ));
1430 }
1431 if tag.is_some() {
1432 return Err(serde::de::Error::custom(
1433 "cannot specify both `path` and `tag`",
1434 ));
1435 }
1436 if branch.is_some() {
1437 return Err(serde::de::Error::custom(
1438 "cannot specify both `path` and `branch`",
1439 ));
1440 }
1441
1442 if editable == Some(true) && package == Some(false) {
1444 return Err(serde::de::Error::custom(
1445 "cannot specify both `editable = true` and `package = false`",
1446 ));
1447 }
1448
1449 return Ok(Self::Path {
1450 path,
1451 editable,
1452 package,
1453 marker,
1454 extra,
1455 group,
1456 });
1457 }
1458
1459 if let Some(index) = index {
1461 if workspace.is_some() {
1462 return Err(serde::de::Error::custom(
1463 "cannot specify both `index` and `workspace`",
1464 ));
1465 }
1466 if git.is_some() {
1467 return Err(serde::de::Error::custom(
1468 "cannot specify both `index` and `git`",
1469 ));
1470 }
1471 if url.is_some() {
1472 return Err(serde::de::Error::custom(
1473 "cannot specify both `index` and `url`",
1474 ));
1475 }
1476 if path.is_some() {
1477 return Err(serde::de::Error::custom(
1478 "cannot specify both `index` and `path`",
1479 ));
1480 }
1481 if rev.is_some() {
1482 return Err(serde::de::Error::custom(
1483 "cannot specify both `index` and `rev`",
1484 ));
1485 }
1486 if tag.is_some() {
1487 return Err(serde::de::Error::custom(
1488 "cannot specify both `index` and `tag`",
1489 ));
1490 }
1491 if branch.is_some() {
1492 return Err(serde::de::Error::custom(
1493 "cannot specify both `index` and `branch`",
1494 ));
1495 }
1496 if editable.is_some() {
1497 return Err(serde::de::Error::custom(
1498 "cannot specify both `index` and `editable`",
1499 ));
1500 }
1501 if package.is_some() {
1502 return Err(serde::de::Error::custom(
1503 "cannot specify both `index` and `package`",
1504 ));
1505 }
1506
1507 return Ok(Self::Registry {
1508 index,
1509 marker,
1510 extra,
1511 group,
1512 });
1513 }
1514
1515 if let Some(workspace) = workspace {
1517 if index.is_some() {
1518 return Err(serde::de::Error::custom(
1519 "cannot specify both `workspace` and `index`",
1520 ));
1521 }
1522 if git.is_some() {
1523 return Err(serde::de::Error::custom(
1524 "cannot specify both `workspace` and `git`",
1525 ));
1526 }
1527 if url.is_some() {
1528 return Err(serde::de::Error::custom(
1529 "cannot specify both `workspace` and `url`",
1530 ));
1531 }
1532 if path.is_some() {
1533 return Err(serde::de::Error::custom(
1534 "cannot specify both `workspace` and `path`",
1535 ));
1536 }
1537 if rev.is_some() {
1538 return Err(serde::de::Error::custom(
1539 "cannot specify both `workspace` and `rev`",
1540 ));
1541 }
1542 if tag.is_some() {
1543 return Err(serde::de::Error::custom(
1544 "cannot specify both `workspace` and `tag`",
1545 ));
1546 }
1547 if branch.is_some() {
1548 return Err(serde::de::Error::custom(
1549 "cannot specify both `workspace` and `branch`",
1550 ));
1551 }
1552 if package.is_some() {
1553 return Err(serde::de::Error::custom(
1554 "cannot specify both `workspace` and `package`",
1555 ));
1556 }
1557
1558 return Ok(Self::Workspace {
1559 workspace,
1560 editable,
1561 marker,
1562 extra,
1563 group,
1564 });
1565 }
1566
1567 Err(serde::de::Error::custom(
1569 "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1570 ))
1571 }
1572}
1573
1574#[derive(Error, Debug)]
1575pub enum SourceError {
1576 #[error("Failed to resolve Git reference: `{0}`")]
1577 UnresolvedReference(String),
1578 #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1579 WorkspacePackageGit(String),
1580 #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1581 WorkspacePackageUrl(String),
1582 #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1583 WorkspacePackageFile(String),
1584 #[error(
1585 "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1586 )]
1587 UnusedRev(String, String),
1588 #[error(
1589 "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1590 )]
1591 UnusedTag(String, String),
1592 #[error(
1593 "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1594 )]
1595 UnusedBranch(String, String),
1596 #[error(
1597 "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1598 )]
1599 UnusedLfs(String),
1600 #[error(
1601 "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1602 )]
1603 UnusedEditable(String),
1604 #[error("Failed to resolve absolute path")]
1605 Absolute(#[from] std::io::Error),
1606 #[error("Path contains invalid characters: `{}`", _0.display())]
1607 NonUtf8Path(PathBuf),
1608 #[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()
1609 )]
1610 OverlappingMarkers(String, String, String),
1611 #[error(
1612 "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1613 )]
1614 MissingMarkers,
1615 #[error("Must provide at least one source")]
1616 EmptySources,
1617}
1618
1619impl Source {
1620 pub fn from_requirement(
1621 name: &PackageName,
1622 source: RequirementSource,
1623 workspace: bool,
1624 editable: Option<bool>,
1625 index: Option<IndexName>,
1626 rev: Option<String>,
1627 tag: Option<String>,
1628 branch: Option<String>,
1629 lfs: GitLfsSetting,
1630 root: &Path,
1631 existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1632 ) -> Result<Option<Self>, SourceError> {
1633 if !matches!(source, RequirementSource::Git { .. })
1635 && (branch.is_some()
1636 || tag.is_some()
1637 || rev.is_some()
1638 || matches!(lfs, GitLfsSetting::Enabled { .. }))
1639 {
1640 if let Some(sources) = existing_sources {
1641 if let Some(package_sources) = sources.get(name) {
1642 for existing_source in package_sources.iter() {
1643 if let Self::Git {
1644 git,
1645 subdirectory,
1646 marker,
1647 extra,
1648 group,
1649 ..
1650 } = existing_source
1651 {
1652 return Ok(Some(Self::Git {
1653 git: git.clone(),
1654 subdirectory: subdirectory.clone(),
1655 rev,
1656 tag,
1657 branch,
1658 lfs: lfs.into(),
1659 marker: *marker,
1660 extra: extra.clone(),
1661 group: group.clone(),
1662 }));
1663 }
1664 }
1665 }
1666 }
1667 if let Some(rev) = rev {
1668 return Err(SourceError::UnusedRev(name.to_string(), rev));
1669 }
1670 if let Some(tag) = tag {
1671 return Err(SourceError::UnusedTag(name.to_string(), tag));
1672 }
1673 if let Some(branch) = branch {
1674 return Err(SourceError::UnusedBranch(name.to_string(), branch));
1675 }
1676 if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1677 return Err(SourceError::UnusedLfs(name.to_string()));
1678 }
1679 }
1680
1681 if !workspace {
1683 if !matches!(source, RequirementSource::Directory { .. }) {
1684 if editable == Some(true) {
1685 return Err(SourceError::UnusedEditable(name.to_string()));
1686 }
1687 }
1688 }
1689
1690 if workspace {
1692 return match source {
1693 RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1694 Ok(Some(Self::Workspace {
1695 workspace: true,
1696 editable,
1697 marker: MarkerTree::TRUE,
1698 extra: None,
1699 group: None,
1700 }))
1701 }
1702 RequirementSource::Url { .. } => {
1703 Err(SourceError::WorkspacePackageUrl(name.to_string()))
1704 }
1705 RequirementSource::Git { .. } => {
1706 Err(SourceError::WorkspacePackageGit(name.to_string()))
1707 }
1708 RequirementSource::Path { .. } => {
1709 Err(SourceError::WorkspacePackageFile(name.to_string()))
1710 }
1711 };
1712 }
1713
1714 let source = match source {
1715 RequirementSource::Registry { index: Some(_), .. } => {
1716 return Ok(None);
1717 }
1718 RequirementSource::Registry { index: None, .. } => {
1719 if let Some(index) = index {
1720 Self::Registry {
1721 index,
1722 marker: MarkerTree::TRUE,
1723 extra: None,
1724 group: None,
1725 }
1726 } else {
1727 return Ok(None);
1728 }
1729 }
1730 RequirementSource::Path { install_path, .. } => Self::Path {
1731 editable: None,
1732 package: None,
1733 path: PortablePathBuf::from(
1734 relative_to(&install_path, root)
1735 .or_else(|_| std::path::absolute(&install_path))
1736 .map_err(SourceError::Absolute)?
1737 .into_boxed_path(),
1738 ),
1739 marker: MarkerTree::TRUE,
1740 extra: None,
1741 group: None,
1742 },
1743 RequirementSource::Directory {
1744 install_path,
1745 editable: is_editable,
1746 ..
1747 } => Self::Path {
1748 editable: editable.or(is_editable),
1749 package: None,
1750 path: PortablePathBuf::from(
1751 relative_to(&install_path, root)
1752 .or_else(|_| std::path::absolute(&install_path))
1753 .map_err(SourceError::Absolute)?
1754 .into_boxed_path(),
1755 ),
1756 marker: MarkerTree::TRUE,
1757 extra: None,
1758 group: None,
1759 },
1760 RequirementSource::Url {
1761 location,
1762 subdirectory,
1763 ..
1764 } => Self::Url {
1765 url: location,
1766 subdirectory: subdirectory.map(PortablePathBuf::from),
1767 marker: MarkerTree::TRUE,
1768 extra: None,
1769 group: None,
1770 },
1771 RequirementSource::Git {
1772 git, subdirectory, ..
1773 } => {
1774 if rev.is_none() && tag.is_none() && branch.is_none() {
1775 let rev = match git.reference() {
1776 GitReference::Branch(rev) => Some(rev),
1777 GitReference::Tag(rev) => Some(rev),
1778 GitReference::BranchOrTag(rev) => Some(rev),
1779 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1780 GitReference::NamedRef(rev) => Some(rev),
1781 GitReference::DefaultBranch => None,
1782 };
1783 Self::Git {
1784 rev: rev.cloned(),
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 } else {
1795 Self::Git {
1796 rev,
1797 tag,
1798 branch,
1799 lfs: lfs.into(),
1800 git: git.repository().clone(),
1801 subdirectory: subdirectory.map(PortablePathBuf::from),
1802 marker: MarkerTree::TRUE,
1803 extra: None,
1804 group: None,
1805 }
1806 }
1807 }
1808 };
1809
1810 Ok(Some(source))
1811 }
1812
1813 pub fn marker(&self) -> MarkerTree {
1815 match self {
1816 Self::Git { marker, .. } => *marker,
1817 Self::Url { marker, .. } => *marker,
1818 Self::Path { marker, .. } => *marker,
1819 Self::Registry { marker, .. } => *marker,
1820 Self::Workspace { marker, .. } => *marker,
1821 }
1822 }
1823
1824 pub fn extra(&self) -> Option<&ExtraName> {
1826 match self {
1827 Self::Git { extra, .. } => extra.as_ref(),
1828 Self::Url { extra, .. } => extra.as_ref(),
1829 Self::Path { extra, .. } => extra.as_ref(),
1830 Self::Registry { extra, .. } => extra.as_ref(),
1831 Self::Workspace { extra, .. } => extra.as_ref(),
1832 }
1833 }
1834
1835 pub fn group(&self) -> Option<&GroupName> {
1837 match self {
1838 Self::Git { group, .. } => group.as_ref(),
1839 Self::Url { group, .. } => group.as_ref(),
1840 Self::Path { group, .. } => group.as_ref(),
1841 Self::Registry { group, .. } => group.as_ref(),
1842 Self::Workspace { group, .. } => group.as_ref(),
1843 }
1844 }
1845}
1846
1847#[derive(Debug, Clone, PartialEq, Eq)]
1849pub enum DependencyType {
1850 Production,
1852 Dev,
1854 Optional(ExtraName),
1856 Group(GroupName),
1858}
1859
1860#[derive(Debug, Clone, PartialEq, Eq)]
1861#[cfg_attr(test, derive(Serialize))]
1862pub struct BuildBackendSettingsSchema;
1863
1864impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1865 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1866 where
1867 D: Deserializer<'de>,
1868 {
1869 Ok(Self)
1870 }
1871}
1872
1873#[cfg(feature = "schemars")]
1874impl schemars::JsonSchema for BuildBackendSettingsSchema {
1875 fn schema_name() -> Cow<'static, str> {
1876 BuildBackendSettings::schema_name()
1877 }
1878
1879 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1880 BuildBackendSettings::json_schema(generator)
1881 }
1882}
1883
1884impl OptionsMetadata for BuildBackendSettingsSchema {
1885 fn record(visit: &mut dyn Visit) {
1886 BuildBackendSettings::record(visit);
1887 }
1888
1889 fn documentation() -> Option<&'static str> {
1890 BuildBackendSettings::documentation()
1891 }
1892
1893 fn metadata() -> OptionSet
1894 where
1895 Self: Sized + 'static,
1896 {
1897 BuildBackendSettings::metadata()
1898 }
1899}