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>
285where
286 D: Deserializer<'de>,
287{
288 let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
289 if let Some(indexes) = indexes.as_ref() {
290 let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
291 let mut seen_default = false;
292 for index in indexes {
293 if let Some(name) = index.name.as_ref() {
294 if !seen_names.insert(name) {
295 return Err(serde::de::Error::custom(format!(
296 "duplicate index name `{name}`"
297 )));
298 }
299 }
300 if index.default {
301 if seen_default {
302 return Err(serde::de::Error::custom(
303 "found multiple indexes with `default = true`; only one index may be marked as default",
304 ));
305 }
306 seen_default = true;
307 }
308 }
309 }
310 Ok(indexes)
311}
312
313#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
316#[cfg_attr(test, derive(Serialize))]
317#[serde(rename_all = "kebab-case")]
318#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
319pub struct ToolUv {
320 #[option(
328 default = "{}",
329 value_type = "dict",
330 example = r#"
331 [tool.uv.sources]
332 httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
333 pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
334 pydantic = { path = "/path/to/pydantic", editable = true }
335 "#
336 )]
337 pub sources: Option<ToolUvSources>,
338
339 #[option(
367 default = "[]",
368 value_type = "dict",
369 example = r#"
370 [[tool.uv.index]]
371 name = "pytorch"
372 url = "https://download.pytorch.org/whl/cu121"
373 "#
374 )]
375 #[serde(deserialize_with = "deserialize_index_vec", default)]
376 pub index: Option<Vec<Index>>,
377
378 #[option_group]
380 pub workspace: Option<ToolUvWorkspace>,
381
382 #[option(
385 default = r#"true"#,
386 value_type = "bool",
387 example = r#"
388 managed = false
389 "#
390 )]
391 pub managed: Option<bool>,
392
393 #[option(
404 default = r#"true"#,
405 value_type = "bool",
406 example = r#"
407 package = false
408 "#
409 )]
410 pub package: Option<bool>,
411
412 #[option(
416 default = r#"["dev"]"#,
417 value_type = r#"str | list[str]"#,
418 example = r#"
419 default-groups = ["docs"]
420 "#
421 )]
422 pub default_groups: Option<DefaultGroups>,
423
424 #[option(
433 default = "[]",
434 value_type = "dict",
435 example = r#"
436 [tool.uv.dependency-groups]
437 my-group = {requires-python = ">=3.12"}
438 "#
439 )]
440 pub dependency_groups: Option<ToolUvDependencyGroups>,
441
442 #[cfg_attr(
452 feature = "schemars",
453 schemars(
454 with = "Option<Vec<String>>",
455 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
456 )
457 )]
458 #[option(
459 default = "[]",
460 value_type = "list[str]",
461 example = r#"
462 dev-dependencies = ["ruff==0.5.0"]
463 "#
464 )]
465 pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
466
467 #[cfg_attr(
486 feature = "schemars",
487 schemars(
488 with = "Option<Vec<String>>",
489 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
490 )
491 )]
492 #[option(
493 default = "[]",
494 value_type = "list[str]",
495 example = r#"
496 # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
497 # a different version.
498 override-dependencies = ["werkzeug==2.3.0"]
499 "#
500 )]
501 pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
502
503 #[cfg_attr(
518 feature = "schemars",
519 schemars(
520 with = "Option<Vec<String>>",
521 description = "Package names to exclude, e.g., `werkzeug`, `numpy`."
522 )
523 )]
524 #[option(
525 default = "[]",
526 value_type = "list[str]",
527 example = r#"
528 # Exclude Werkzeug from being installed, even if transitive dependencies request it.
529 exclude-dependencies = ["werkzeug"]
530 "#
531 )]
532 pub exclude_dependencies: Option<Vec<PackageName>>,
533
534 #[cfg_attr(
548 feature = "schemars",
549 schemars(
550 with = "Option<Vec<String>>",
551 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
552 )
553 )]
554 #[option(
555 default = "[]",
556 value_type = "list[str]",
557 example = r#"
558 # Ensure that the grpcio version is always less than 1.65, if it's requested by a
559 # direct or transitive dependency.
560 constraint-dependencies = ["grpcio<1.65"]
561 "#
562 )]
563 pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
564
565 #[cfg_attr(
579 feature = "schemars",
580 schemars(
581 with = "Option<Vec<String>>",
582 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
583 )
584 )]
585 #[option(
586 default = "[]",
587 value_type = "list[str]",
588 example = r#"
589 # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
590 # on setuptools.
591 build-constraint-dependencies = ["setuptools==60.0.0"]
592 "#
593 )]
594 pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
595
596 #[cfg_attr(
605 feature = "schemars",
606 schemars(
607 with = "Option<Vec<String>>",
608 description = "A list of environment markers, e.g., `python_version >= '3.6'`."
609 )
610 )]
611 #[option(
612 default = "[]",
613 value_type = "str | list[str]",
614 example = r#"
615 # Resolve for macOS, but not for Linux or Windows.
616 environments = ["sys_platform == 'darwin'"]
617 "#
618 )]
619 pub environments: Option<SupportedEnvironments>,
620
621 #[cfg_attr(
641 feature = "schemars",
642 schemars(
643 with = "Option<Vec<String>>",
644 description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
645 )
646 )]
647 #[option(
648 default = "[]",
649 value_type = "str | list[str]",
650 example = r#"
651 # Require that the package is available for macOS ARM and x86 (Intel).
652 required-environments = [
653 "sys_platform == 'darwin' and platform_machine == 'arm64'",
654 "sys_platform == 'darwin' and platform_machine == 'x86_64'",
655 ]
656 "#
657 )]
658 pub required_environments: Option<SupportedEnvironments>,
659
660 #[cfg_attr(
675 feature = "schemars",
676 schemars(description = "A list of sets of conflicting groups or extras.")
677 )]
678 #[option(
679 default = r#"[]"#,
680 value_type = "list[list[dict]]",
681 example = r#"
682 # Require that `package[extra1]` and `package[extra2]` are resolved
683 # in different forks so that they cannot conflict with one another.
684 conflicts = [
685 [
686 { extra = "extra1" },
687 { extra = "extra2" },
688 ]
689 ]
690
691 # Require that the dependency groups `group1` and `group2`
692 # are resolved in different forks so that they cannot conflict
693 # with one another.
694 conflicts = [
695 [
696 { group = "group1" },
697 { group = "group2" },
698 ]
699 ]
700 "#
701 )]
702 pub conflicts: Option<SchemaConflicts>,
703
704 #[option_group]
711 pub build_backend: Option<BuildBackendSettingsSchema>,
712}
713
714#[derive(Default, Debug, Clone, PartialEq, Eq)]
715#[cfg_attr(test, derive(Serialize))]
716#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
717pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
718
719impl ToolUvSources {
720 pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
722 &self.0
723 }
724
725 #[must_use]
727 pub fn into_inner(self) -> BTreeMap<PackageName, Sources> {
728 self.0
729 }
730}
731
732impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
734 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
735 where
736 D: Deserializer<'de>,
737 {
738 deserialize_unique_map(deserializer, |key: &PackageName| {
739 format!("duplicate sources for package `{key}`")
740 })
741 .map(ToolUvSources)
742 }
743}
744
745#[derive(Default, Debug, Clone, PartialEq, Eq)]
746#[cfg_attr(test, derive(Serialize))]
747#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
748pub struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
749
750impl ToolUvDependencyGroups {
751 pub fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
753 &self.0
754 }
755
756 #[must_use]
758 pub fn into_inner(self) -> BTreeMap<GroupName, DependencyGroupSettings> {
759 self.0
760 }
761}
762
763impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
765 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
766 where
767 D: Deserializer<'de>,
768 {
769 deserialize_unique_map(deserializer, |key: &GroupName| {
770 format!("duplicate settings for dependency group `{key}`")
771 })
772 .map(ToolUvDependencyGroups)
773 }
774}
775
776#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
777#[cfg_attr(test, derive(Serialize))]
778#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
779#[serde(rename_all = "kebab-case")]
780pub struct DependencyGroupSettings {
781 #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
783 pub requires_python: Option<VersionSpecifiers>,
784}
785
786#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
787#[serde(untagged, rename_all = "kebab-case")]
788#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
789pub enum ExtraBuildDependencyWire {
790 Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
791 #[serde(rename_all = "kebab-case")]
792 Annotated {
793 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
794 match_runtime: bool,
795 },
796}
797
798#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
799#[serde(
800 deny_unknown_fields,
801 from = "ExtraBuildDependencyWire",
802 into = "ExtraBuildDependencyWire"
803)]
804#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
805pub struct ExtraBuildDependency {
806 pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
807 pub match_runtime: bool,
808}
809
810impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
811 fn from(value: ExtraBuildDependency) -> Self {
812 value.requirement
813 }
814}
815
816impl From<ExtraBuildDependencyWire> for ExtraBuildDependency {
817 fn from(wire: ExtraBuildDependencyWire) -> Self {
818 match wire {
819 ExtraBuildDependencyWire::Unannotated(requirement) => Self {
820 requirement,
821 match_runtime: false,
822 },
823 ExtraBuildDependencyWire::Annotated {
824 requirement,
825 match_runtime,
826 } => Self {
827 requirement,
828 match_runtime,
829 },
830 }
831 }
832}
833
834impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
835 fn from(item: ExtraBuildDependency) -> Self {
836 Self::Annotated {
837 requirement: item.requirement,
838 match_runtime: item.match_runtime,
839 }
840 }
841}
842
843#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
844#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
845pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
846
847impl std::ops::Deref for ExtraBuildDependencies {
848 type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
849
850 fn deref(&self) -> &Self::Target {
851 &self.0
852 }
853}
854
855impl std::ops::DerefMut for ExtraBuildDependencies {
856 fn deref_mut(&mut self) -> &mut Self::Target {
857 &mut self.0
858 }
859}
860
861impl IntoIterator for ExtraBuildDependencies {
862 type Item = (PackageName, Vec<ExtraBuildDependency>);
863 type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
864
865 fn into_iter(self) -> Self::IntoIter {
866 self.0.into_iter()
867 }
868}
869
870impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
871 fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
872 iter: T,
873 ) -> Self {
874 Self(iter.into_iter().collect())
875 }
876}
877
878impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
880 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
881 where
882 D: Deserializer<'de>,
883 {
884 deserialize_unique_map(deserializer, |key: &PackageName| {
885 format!("duplicate extra-build-dependencies for `{key}`")
886 })
887 .map(ExtraBuildDependencies)
888 }
889}
890
891#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
892#[cfg_attr(test, derive(Serialize))]
893#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
894#[serde(rename_all = "kebab-case", deny_unknown_fields)]
895pub struct ToolUvWorkspace {
896 #[option(
902 default = "[]",
903 value_type = "list[str]",
904 example = r#"
905 members = ["member1", "path/to/member2", "libs/*"]
906 "#
907 )]
908 pub members: Option<Vec<SerdePattern>>,
909 #[option(
916 default = "[]",
917 value_type = "list[str]",
918 example = r#"
919 exclude = ["member1", "path/to/member2", "libs/*"]
920 "#
921 )]
922 pub exclude: Option<Vec<SerdePattern>>,
923}
924
925#[derive(Debug, Clone, PartialEq, Eq)]
927pub struct SerdePattern(Pattern);
928
929impl serde::ser::Serialize for SerdePattern {
930 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
931 where
932 S: serde::ser::Serializer,
933 {
934 self.0.as_str().serialize(serializer)
935 }
936}
937
938impl<'de> serde::Deserialize<'de> for SerdePattern {
939 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
940 struct Visitor;
941
942 impl serde::de::Visitor<'_> for Visitor {
943 type Value = SerdePattern;
944
945 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
946 f.write_str("a string")
947 }
948
949 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
950 Pattern::from_str(v)
951 .map(SerdePattern)
952 .map_err(serde::de::Error::custom)
953 }
954 }
955
956 deserializer.deserialize_str(Visitor)
957 }
958}
959
960#[cfg(feature = "schemars")]
961impl schemars::JsonSchema for SerdePattern {
962 fn schema_name() -> Cow<'static, str> {
963 Cow::Borrowed("SerdePattern")
964 }
965
966 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
967 <String as schemars::JsonSchema>::json_schema(generator)
968 }
969}
970
971impl Deref for SerdePattern {
972 type Target = Pattern;
973
974 fn deref(&self) -> &Self::Target {
975 &self.0
976 }
977}
978
979#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
980#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
981#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
982pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
983
984impl Sources {
985 pub fn iter(&self) -> impl Iterator<Item = &Source> {
991 self.0.iter()
992 }
993
994 pub fn is_empty(&self) -> bool {
996 self.0.is_empty()
997 }
998
999 pub fn len(&self) -> usize {
1001 self.0.len()
1002 }
1003}
1004
1005impl FromIterator<Source> for Sources {
1006 fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
1007 Self(iter.into_iter().collect())
1008 }
1009}
1010
1011impl IntoIterator for Sources {
1012 type Item = Source;
1013 type IntoIter = std::vec::IntoIter<Source>;
1014
1015 fn into_iter(self) -> Self::IntoIter {
1016 self.0.into_iter()
1017 }
1018}
1019
1020#[derive(Debug, Clone, PartialEq, Eq)]
1021#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1022enum SourcesWire {
1023 One(Source),
1024 Many(Vec<Source>),
1025}
1026
1027impl<'de> serde::de::Deserialize<'de> for SourcesWire {
1028 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1029 where
1030 D: Deserializer<'de>,
1031 {
1032 struct Visitor;
1033
1034 impl<'de> serde::de::Visitor<'de> for Visitor {
1035 type Value = SourcesWire;
1036
1037 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1038 formatter.write_str("a single source (as a map) or list of sources")
1039 }
1040
1041 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1042 where
1043 A: SeqAccess<'de>,
1044 {
1045 let sources = serde::de::Deserialize::deserialize(
1046 serde::de::value::SeqAccessDeserializer::new(seq),
1047 )?;
1048 Ok(SourcesWire::Many(sources))
1049 }
1050
1051 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1052 where
1053 M: serde::de::MapAccess<'de>,
1054 {
1055 let source = serde::de::Deserialize::deserialize(
1056 serde::de::value::MapAccessDeserializer::new(&mut map),
1057 )?;
1058 Ok(SourcesWire::One(source))
1059 }
1060 }
1061
1062 deserializer.deserialize_any(Visitor)
1063 }
1064}
1065
1066impl TryFrom<SourcesWire> for Sources {
1067 type Error = SourceError;
1068
1069 fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
1070 match wire {
1071 SourcesWire::One(source) => Ok(Self(vec![source])),
1072 SourcesWire::Many(sources) => {
1073 for (lhs, rhs) in sources.iter().zip(sources.iter().skip(1)) {
1074 if lhs.extra() != rhs.extra() {
1075 continue;
1076 }
1077 if lhs.group() != rhs.group() {
1078 continue;
1079 }
1080
1081 let lhs = lhs.marker();
1082 let rhs = rhs.marker();
1083 if !lhs.is_disjoint(rhs) {
1084 let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
1085 return Err(SourceError::MissingMarkers);
1086 };
1087
1088 let Some(right) = rhs.contents().map(|contents| contents.to_string())
1089 else {
1090 return Err(SourceError::MissingMarkers);
1091 };
1092
1093 let mut hint = lhs.negate();
1094 hint.and(rhs);
1095 let hint = hint
1096 .contents()
1097 .map(|contents| contents.to_string())
1098 .unwrap_or_else(|| "true".to_string());
1099
1100 return Err(SourceError::OverlappingMarkers(left, right, hint));
1101 }
1102 }
1103
1104 if sources.is_empty() {
1106 return Err(SourceError::EmptySources);
1107 }
1108
1109 Ok(Self(sources))
1110 }
1111 }
1112 }
1113}
1114
1115#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1117#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1118#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1119pub enum Source {
1120 Git {
1127 git: DisplaySafeUrl,
1129 subdirectory: Option<PortablePathBuf>,
1131 rev: Option<String>,
1133 tag: Option<String>,
1134 branch: Option<String>,
1135 lfs: Option<bool>,
1137 #[serde(
1138 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1139 serialize_with = "uv_pep508::marker::ser::serialize",
1140 default
1141 )]
1142 marker: MarkerTree,
1143 extra: Option<ExtraName>,
1144 group: Option<GroupName>,
1145 },
1146 Url {
1154 url: DisplaySafeUrl,
1155 subdirectory: Option<PortablePathBuf>,
1158 #[serde(
1159 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1160 serialize_with = "uv_pep508::marker::ser::serialize",
1161 default
1162 )]
1163 marker: MarkerTree,
1164 extra: Option<ExtraName>,
1165 group: Option<GroupName>,
1166 },
1167 Path {
1171 path: PortablePathBuf,
1172 editable: Option<bool>,
1174 package: Option<bool>,
1181 #[serde(
1182 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1183 serialize_with = "uv_pep508::marker::ser::serialize",
1184 default
1185 )]
1186 marker: MarkerTree,
1187 extra: Option<ExtraName>,
1188 group: Option<GroupName>,
1189 },
1190 Registry {
1192 index: IndexName,
1193 #[serde(
1194 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1195 serialize_with = "uv_pep508::marker::ser::serialize",
1196 default
1197 )]
1198 marker: MarkerTree,
1199 extra: Option<ExtraName>,
1200 group: Option<GroupName>,
1201 },
1202 Workspace {
1204 workspace: bool,
1207 editable: Option<bool>,
1209 #[serde(
1210 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1211 serialize_with = "uv_pep508::marker::ser::serialize",
1212 default
1213 )]
1214 marker: MarkerTree,
1215 extra: Option<ExtraName>,
1216 group: Option<GroupName>,
1217 },
1218}
1219
1220impl<'de> Deserialize<'de> for Source {
1223 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1224 where
1225 D: Deserializer<'de>,
1226 {
1227 #[derive(Deserialize, Debug, Clone)]
1228 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1229 struct CatchAll {
1230 git: Option<DisplaySafeUrl>,
1231 subdirectory: Option<PortablePathBuf>,
1232 rev: Option<String>,
1233 tag: Option<String>,
1234 branch: Option<String>,
1235 lfs: Option<bool>,
1236 url: Option<DisplaySafeUrl>,
1237 path: Option<PortablePathBuf>,
1238 editable: Option<bool>,
1239 package: Option<bool>,
1240 index: Option<IndexName>,
1241 workspace: Option<bool>,
1242 #[serde(
1243 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1244 serialize_with = "uv_pep508::marker::ser::serialize",
1245 default
1246 )]
1247 marker: MarkerTree,
1248 extra: Option<ExtraName>,
1249 group: Option<GroupName>,
1250 }
1251
1252 let CatchAll {
1254 git,
1255 subdirectory,
1256 rev,
1257 tag,
1258 branch,
1259 lfs,
1260 url,
1261 path,
1262 editable,
1263 package,
1264 index,
1265 workspace,
1266 marker,
1267 extra,
1268 group,
1269 } = CatchAll::deserialize(deserializer)?;
1270
1271 if extra.is_some() && group.is_some() {
1273 return Err(serde::de::Error::custom(
1274 "cannot specify both `extra` and `group`",
1275 ));
1276 }
1277
1278 if let Some(git) = git {
1280 if index.is_some() {
1281 return Err(serde::de::Error::custom(
1282 "cannot specify both `git` and `index`",
1283 ));
1284 }
1285 if workspace.is_some() {
1286 return Err(serde::de::Error::custom(
1287 "cannot specify both `git` and `workspace`",
1288 ));
1289 }
1290 if path.is_some() {
1291 return Err(serde::de::Error::custom(
1292 "cannot specify both `git` and `path`",
1293 ));
1294 }
1295 if url.is_some() {
1296 return Err(serde::de::Error::custom(
1297 "cannot specify both `git` and `url`",
1298 ));
1299 }
1300 if editable.is_some() {
1301 return Err(serde::de::Error::custom(
1302 "cannot specify both `git` and `editable`",
1303 ));
1304 }
1305 if package.is_some() {
1306 return Err(serde::de::Error::custom(
1307 "cannot specify both `git` and `package`",
1308 ));
1309 }
1310
1311 match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1313 (None, None, None) => {}
1314 (Some(_), None, None) => {}
1315 (None, Some(_), None) => {}
1316 (None, None, Some(_)) => {}
1317 _ => {
1318 return Err(serde::de::Error::custom(
1319 "expected at most one of `rev`, `tag`, or `branch`",
1320 ));
1321 }
1322 }
1323
1324 let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1326 DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1327 } else {
1328 git
1329 };
1330
1331 return Ok(Self::Git {
1332 git,
1333 subdirectory,
1334 rev,
1335 tag,
1336 branch,
1337 lfs,
1338 marker,
1339 extra,
1340 group,
1341 });
1342 }
1343
1344 if let Some(url) = url {
1346 if index.is_some() {
1347 return Err(serde::de::Error::custom(
1348 "cannot specify both `url` and `index`",
1349 ));
1350 }
1351 if workspace.is_some() {
1352 return Err(serde::de::Error::custom(
1353 "cannot specify both `url` and `workspace`",
1354 ));
1355 }
1356 if path.is_some() {
1357 return Err(serde::de::Error::custom(
1358 "cannot specify both `url` and `path`",
1359 ));
1360 }
1361 if git.is_some() {
1362 return Err(serde::de::Error::custom(
1363 "cannot specify both `url` and `git`",
1364 ));
1365 }
1366 if rev.is_some() {
1367 return Err(serde::de::Error::custom(
1368 "cannot specify both `url` and `rev`",
1369 ));
1370 }
1371 if tag.is_some() {
1372 return Err(serde::de::Error::custom(
1373 "cannot specify both `url` and `tag`",
1374 ));
1375 }
1376 if branch.is_some() {
1377 return Err(serde::de::Error::custom(
1378 "cannot specify both `url` and `branch`",
1379 ));
1380 }
1381 if editable.is_some() {
1382 return Err(serde::de::Error::custom(
1383 "cannot specify both `url` and `editable`",
1384 ));
1385 }
1386 if package.is_some() {
1387 return Err(serde::de::Error::custom(
1388 "cannot specify both `url` and `package`",
1389 ));
1390 }
1391
1392 return Ok(Self::Url {
1393 url,
1394 subdirectory,
1395 marker,
1396 extra,
1397 group,
1398 });
1399 }
1400
1401 if let Some(path) = path {
1403 if index.is_some() {
1404 return Err(serde::de::Error::custom(
1405 "cannot specify both `path` and `index`",
1406 ));
1407 }
1408 if workspace.is_some() {
1409 return Err(serde::de::Error::custom(
1410 "cannot specify both `path` and `workspace`",
1411 ));
1412 }
1413 if git.is_some() {
1414 return Err(serde::de::Error::custom(
1415 "cannot specify both `path` and `git`",
1416 ));
1417 }
1418 if url.is_some() {
1419 return Err(serde::de::Error::custom(
1420 "cannot specify both `path` and `url`",
1421 ));
1422 }
1423 if rev.is_some() {
1424 return Err(serde::de::Error::custom(
1425 "cannot specify both `path` and `rev`",
1426 ));
1427 }
1428 if tag.is_some() {
1429 return Err(serde::de::Error::custom(
1430 "cannot specify both `path` and `tag`",
1431 ));
1432 }
1433 if branch.is_some() {
1434 return Err(serde::de::Error::custom(
1435 "cannot specify both `path` and `branch`",
1436 ));
1437 }
1438
1439 if editable == Some(true) && package == Some(false) {
1441 return Err(serde::de::Error::custom(
1442 "cannot specify both `editable = true` and `package = false`",
1443 ));
1444 }
1445
1446 return Ok(Self::Path {
1447 path,
1448 editable,
1449 package,
1450 marker,
1451 extra,
1452 group,
1453 });
1454 }
1455
1456 if let Some(index) = index {
1458 if workspace.is_some() {
1459 return Err(serde::de::Error::custom(
1460 "cannot specify both `index` and `workspace`",
1461 ));
1462 }
1463 if git.is_some() {
1464 return Err(serde::de::Error::custom(
1465 "cannot specify both `index` and `git`",
1466 ));
1467 }
1468 if url.is_some() {
1469 return Err(serde::de::Error::custom(
1470 "cannot specify both `index` and `url`",
1471 ));
1472 }
1473 if path.is_some() {
1474 return Err(serde::de::Error::custom(
1475 "cannot specify both `index` and `path`",
1476 ));
1477 }
1478 if rev.is_some() {
1479 return Err(serde::de::Error::custom(
1480 "cannot specify both `index` and `rev`",
1481 ));
1482 }
1483 if tag.is_some() {
1484 return Err(serde::de::Error::custom(
1485 "cannot specify both `index` and `tag`",
1486 ));
1487 }
1488 if branch.is_some() {
1489 return Err(serde::de::Error::custom(
1490 "cannot specify both `index` and `branch`",
1491 ));
1492 }
1493 if editable.is_some() {
1494 return Err(serde::de::Error::custom(
1495 "cannot specify both `index` and `editable`",
1496 ));
1497 }
1498 if package.is_some() {
1499 return Err(serde::de::Error::custom(
1500 "cannot specify both `index` and `package`",
1501 ));
1502 }
1503
1504 return Ok(Self::Registry {
1505 index,
1506 marker,
1507 extra,
1508 group,
1509 });
1510 }
1511
1512 if let Some(workspace) = workspace {
1514 if index.is_some() {
1515 return Err(serde::de::Error::custom(
1516 "cannot specify both `workspace` and `index`",
1517 ));
1518 }
1519 if git.is_some() {
1520 return Err(serde::de::Error::custom(
1521 "cannot specify both `workspace` and `git`",
1522 ));
1523 }
1524 if url.is_some() {
1525 return Err(serde::de::Error::custom(
1526 "cannot specify both `workspace` and `url`",
1527 ));
1528 }
1529 if path.is_some() {
1530 return Err(serde::de::Error::custom(
1531 "cannot specify both `workspace` and `path`",
1532 ));
1533 }
1534 if rev.is_some() {
1535 return Err(serde::de::Error::custom(
1536 "cannot specify both `workspace` and `rev`",
1537 ));
1538 }
1539 if tag.is_some() {
1540 return Err(serde::de::Error::custom(
1541 "cannot specify both `workspace` and `tag`",
1542 ));
1543 }
1544 if branch.is_some() {
1545 return Err(serde::de::Error::custom(
1546 "cannot specify both `workspace` and `branch`",
1547 ));
1548 }
1549 if package.is_some() {
1550 return Err(serde::de::Error::custom(
1551 "cannot specify both `workspace` and `package`",
1552 ));
1553 }
1554
1555 return Ok(Self::Workspace {
1556 workspace,
1557 editable,
1558 marker,
1559 extra,
1560 group,
1561 });
1562 }
1563
1564 Err(serde::de::Error::custom(
1566 "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1567 ))
1568 }
1569}
1570
1571#[derive(Error, Debug)]
1572pub enum SourceError {
1573 #[error("Failed to resolve Git reference: `{0}`")]
1574 UnresolvedReference(String),
1575 #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1576 WorkspacePackageGit(String),
1577 #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1578 WorkspacePackageUrl(String),
1579 #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1580 WorkspacePackageFile(String),
1581 #[error(
1582 "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1583 )]
1584 UnusedRev(String, String),
1585 #[error(
1586 "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1587 )]
1588 UnusedTag(String, String),
1589 #[error(
1590 "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1591 )]
1592 UnusedBranch(String, String),
1593 #[error(
1594 "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1595 )]
1596 UnusedLfs(String),
1597 #[error(
1598 "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1599 )]
1600 UnusedEditable(String),
1601 #[error("Failed to resolve absolute path")]
1602 Absolute(#[from] std::io::Error),
1603 #[error("Path contains invalid characters: `{}`", _0.display())]
1604 NonUtf8Path(PathBuf),
1605 #[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()
1606 )]
1607 OverlappingMarkers(String, String, String),
1608 #[error(
1609 "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1610 )]
1611 MissingMarkers,
1612 #[error("Must provide at least one source")]
1613 EmptySources,
1614}
1615
1616impl Source {
1617 pub fn from_requirement(
1618 name: &PackageName,
1619 source: RequirementSource,
1620 workspace: bool,
1621 editable: Option<bool>,
1622 index: Option<IndexName>,
1623 rev: Option<String>,
1624 tag: Option<String>,
1625 branch: Option<String>,
1626 lfs: GitLfsSetting,
1627 root: &Path,
1628 existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1629 ) -> Result<Option<Self>, SourceError> {
1630 if !matches!(source, RequirementSource::Git { .. })
1632 && (branch.is_some()
1633 || tag.is_some()
1634 || rev.is_some()
1635 || matches!(lfs, GitLfsSetting::Enabled { .. }))
1636 {
1637 if let Some(sources) = existing_sources {
1638 if let Some(package_sources) = sources.get(name) {
1639 for existing_source in package_sources.iter() {
1640 if let Self::Git {
1641 git,
1642 subdirectory,
1643 marker,
1644 extra,
1645 group,
1646 ..
1647 } = existing_source
1648 {
1649 return Ok(Some(Self::Git {
1650 git: git.clone(),
1651 subdirectory: subdirectory.clone(),
1652 rev,
1653 tag,
1654 branch,
1655 lfs: lfs.into(),
1656 marker: *marker,
1657 extra: extra.clone(),
1658 group: group.clone(),
1659 }));
1660 }
1661 }
1662 }
1663 }
1664 if let Some(rev) = rev {
1665 return Err(SourceError::UnusedRev(name.to_string(), rev));
1666 }
1667 if let Some(tag) = tag {
1668 return Err(SourceError::UnusedTag(name.to_string(), tag));
1669 }
1670 if let Some(branch) = branch {
1671 return Err(SourceError::UnusedBranch(name.to_string(), branch));
1672 }
1673 if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1674 return Err(SourceError::UnusedLfs(name.to_string()));
1675 }
1676 }
1677
1678 if !workspace {
1680 if !matches!(source, RequirementSource::Directory { .. }) {
1681 if editable == Some(true) {
1682 return Err(SourceError::UnusedEditable(name.to_string()));
1683 }
1684 }
1685 }
1686
1687 if workspace {
1689 return match source {
1690 RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1691 Ok(Some(Self::Workspace {
1692 workspace: true,
1693 editable,
1694 marker: MarkerTree::TRUE,
1695 extra: None,
1696 group: None,
1697 }))
1698 }
1699 RequirementSource::Url { .. } => {
1700 Err(SourceError::WorkspacePackageUrl(name.to_string()))
1701 }
1702 RequirementSource::Git { .. } => {
1703 Err(SourceError::WorkspacePackageGit(name.to_string()))
1704 }
1705 RequirementSource::Path { .. } => {
1706 Err(SourceError::WorkspacePackageFile(name.to_string()))
1707 }
1708 };
1709 }
1710
1711 let source = match source {
1712 RequirementSource::Registry { index: Some(_), .. } => {
1713 return Ok(None);
1714 }
1715 RequirementSource::Registry { index: None, .. } => {
1716 if let Some(index) = index {
1717 Self::Registry {
1718 index,
1719 marker: MarkerTree::TRUE,
1720 extra: None,
1721 group: None,
1722 }
1723 } else {
1724 return Ok(None);
1725 }
1726 }
1727 RequirementSource::Path { install_path, .. } => Self::Path {
1728 editable: None,
1729 package: None,
1730 path: PortablePathBuf::from(
1731 relative_to(&install_path, root)
1732 .or_else(|_| std::path::absolute(&install_path))
1733 .map_err(SourceError::Absolute)?
1734 .into_boxed_path(),
1735 ),
1736 marker: MarkerTree::TRUE,
1737 extra: None,
1738 group: None,
1739 },
1740 RequirementSource::Directory {
1741 install_path,
1742 editable: is_editable,
1743 ..
1744 } => Self::Path {
1745 editable: editable.or(is_editable),
1746 package: None,
1747 path: PortablePathBuf::from(
1748 relative_to(&install_path, root)
1749 .or_else(|_| std::path::absolute(&install_path))
1750 .map_err(SourceError::Absolute)?
1751 .into_boxed_path(),
1752 ),
1753 marker: MarkerTree::TRUE,
1754 extra: None,
1755 group: None,
1756 },
1757 RequirementSource::Url {
1758 location,
1759 subdirectory,
1760 ..
1761 } => Self::Url {
1762 url: location,
1763 subdirectory: subdirectory.map(PortablePathBuf::from),
1764 marker: MarkerTree::TRUE,
1765 extra: None,
1766 group: None,
1767 },
1768 RequirementSource::Git {
1769 git, subdirectory, ..
1770 } => {
1771 if rev.is_none() && tag.is_none() && branch.is_none() {
1772 let rev = match git.reference() {
1773 GitReference::Branch(rev) => Some(rev),
1774 GitReference::Tag(rev) => Some(rev),
1775 GitReference::BranchOrTag(rev) => Some(rev),
1776 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1777 GitReference::NamedRef(rev) => Some(rev),
1778 GitReference::DefaultBranch => None,
1779 };
1780 Self::Git {
1781 rev: rev.cloned(),
1782 tag,
1783 branch,
1784 lfs: lfs.into(),
1785 git: git.repository().clone(),
1786 subdirectory: subdirectory.map(PortablePathBuf::from),
1787 marker: MarkerTree::TRUE,
1788 extra: None,
1789 group: None,
1790 }
1791 } else {
1792 Self::Git {
1793 rev,
1794 tag,
1795 branch,
1796 lfs: lfs.into(),
1797 git: git.repository().clone(),
1798 subdirectory: subdirectory.map(PortablePathBuf::from),
1799 marker: MarkerTree::TRUE,
1800 extra: None,
1801 group: None,
1802 }
1803 }
1804 }
1805 };
1806
1807 Ok(Some(source))
1808 }
1809
1810 pub fn marker(&self) -> MarkerTree {
1812 match self {
1813 Self::Git { marker, .. } => *marker,
1814 Self::Url { marker, .. } => *marker,
1815 Self::Path { marker, .. } => *marker,
1816 Self::Registry { marker, .. } => *marker,
1817 Self::Workspace { marker, .. } => *marker,
1818 }
1819 }
1820
1821 pub fn extra(&self) -> Option<&ExtraName> {
1823 match self {
1824 Self::Git { extra, .. } => extra.as_ref(),
1825 Self::Url { extra, .. } => extra.as_ref(),
1826 Self::Path { extra, .. } => extra.as_ref(),
1827 Self::Registry { extra, .. } => extra.as_ref(),
1828 Self::Workspace { extra, .. } => extra.as_ref(),
1829 }
1830 }
1831
1832 pub fn group(&self) -> Option<&GroupName> {
1834 match self {
1835 Self::Git { group, .. } => group.as_ref(),
1836 Self::Url { group, .. } => group.as_ref(),
1837 Self::Path { group, .. } => group.as_ref(),
1838 Self::Registry { group, .. } => group.as_ref(),
1839 Self::Workspace { group, .. } => group.as_ref(),
1840 }
1841 }
1842}
1843
1844#[derive(Debug, Clone, PartialEq, Eq)]
1846pub enum DependencyType {
1847 Production,
1849 Dev,
1851 Optional(ExtraName),
1853 Group(GroupName),
1855}
1856
1857#[derive(Debug, Clone, PartialEq, Eq)]
1858#[cfg_attr(test, derive(Serialize))]
1859pub struct BuildBackendSettingsSchema;
1860
1861impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1862 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1863 where
1864 D: Deserializer<'de>,
1865 {
1866 Ok(Self)
1867 }
1868}
1869
1870#[cfg(feature = "schemars")]
1871impl schemars::JsonSchema for BuildBackendSettingsSchema {
1872 fn schema_name() -> Cow<'static, str> {
1873 BuildBackendSettings::schema_name()
1874 }
1875
1876 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1877 BuildBackendSettings::json_schema(generator)
1878 }
1879}
1880
1881impl OptionsMetadata for BuildBackendSettingsSchema {
1882 fn record(visit: &mut dyn Visit) {
1883 BuildBackendSettings::record(visit);
1884 }
1885
1886 fn documentation() -> Option<&'static str> {
1887 BuildBackendSettings::documentation()
1888 }
1889
1890 fn metadata() -> OptionSet
1891 where
1892 Self: Sized + 'static,
1893 {
1894 BuildBackendSettings::metadata()
1895 }
1896}