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))]
1012enum SourcesWire {
1013 One(Source),
1014 Many(Vec<Source>),
1015}
1016
1017impl<'de> serde::de::Deserialize<'de> for SourcesWire {
1018 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1019 where
1020 D: Deserializer<'de>,
1021 {
1022 struct Visitor;
1023
1024 impl<'de> serde::de::Visitor<'de> for Visitor {
1025 type Value = SourcesWire;
1026
1027 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1028 formatter.write_str("a single source (as a map) or list of sources")
1029 }
1030
1031 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1032 where
1033 A: SeqAccess<'de>,
1034 {
1035 let sources = serde::de::Deserialize::deserialize(
1036 serde::de::value::SeqAccessDeserializer::new(seq),
1037 )?;
1038 Ok(SourcesWire::Many(sources))
1039 }
1040
1041 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1042 where
1043 M: serde::de::MapAccess<'de>,
1044 {
1045 let source = serde::de::Deserialize::deserialize(
1046 serde::de::value::MapAccessDeserializer::new(&mut map),
1047 )?;
1048 Ok(SourcesWire::One(source))
1049 }
1050 }
1051
1052 deserializer.deserialize_any(Visitor)
1053 }
1054}
1055
1056impl TryFrom<SourcesWire> for Sources {
1057 type Error = SourceError;
1058
1059 fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
1060 match wire {
1061 SourcesWire::One(source) => Ok(Self(vec![source])),
1062 SourcesWire::Many(sources) => {
1063 for (lhs, rhs) in sources.iter().zip(sources.iter().skip(1)) {
1064 if lhs.extra() != rhs.extra() {
1065 continue;
1066 }
1067 if lhs.group() != rhs.group() {
1068 continue;
1069 }
1070
1071 let lhs = lhs.marker();
1072 let rhs = rhs.marker();
1073 if !lhs.is_disjoint(rhs) {
1074 let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
1075 return Err(SourceError::MissingMarkers);
1076 };
1077
1078 let Some(right) = rhs.contents().map(|contents| contents.to_string())
1079 else {
1080 return Err(SourceError::MissingMarkers);
1081 };
1082
1083 let mut hint = lhs.negate();
1084 hint.and(rhs);
1085 let hint = hint
1086 .contents()
1087 .map(|contents| contents.to_string())
1088 .unwrap_or_else(|| "true".to_string());
1089
1090 return Err(SourceError::OverlappingMarkers(left, right, hint));
1091 }
1092 }
1093
1094 if sources.is_empty() {
1096 return Err(SourceError::EmptySources);
1097 }
1098
1099 Ok(Self(sources))
1100 }
1101 }
1102 }
1103}
1104
1105#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1107#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1108#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1109pub enum Source {
1110 Git {
1117 git: DisplaySafeUrl,
1119 subdirectory: Option<PortablePathBuf>,
1121 rev: Option<String>,
1123 tag: Option<String>,
1124 branch: Option<String>,
1125 lfs: Option<bool>,
1127 #[serde(
1128 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1129 serialize_with = "uv_pep508::marker::ser::serialize",
1130 default
1131 )]
1132 marker: MarkerTree,
1133 extra: Option<ExtraName>,
1134 group: Option<GroupName>,
1135 },
1136 Url {
1144 url: DisplaySafeUrl,
1145 subdirectory: Option<PortablePathBuf>,
1148 #[serde(
1149 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1150 serialize_with = "uv_pep508::marker::ser::serialize",
1151 default
1152 )]
1153 marker: MarkerTree,
1154 extra: Option<ExtraName>,
1155 group: Option<GroupName>,
1156 },
1157 Path {
1161 path: PortablePathBuf,
1162 editable: Option<bool>,
1164 package: Option<bool>,
1171 #[serde(
1172 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1173 serialize_with = "uv_pep508::marker::ser::serialize",
1174 default
1175 )]
1176 marker: MarkerTree,
1177 extra: Option<ExtraName>,
1178 group: Option<GroupName>,
1179 },
1180 Registry {
1182 index: IndexName,
1183 #[serde(
1184 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1185 serialize_with = "uv_pep508::marker::ser::serialize",
1186 default
1187 )]
1188 marker: MarkerTree,
1189 extra: Option<ExtraName>,
1190 group: Option<GroupName>,
1191 },
1192 Workspace {
1194 workspace: bool,
1197 editable: Option<bool>,
1199 #[serde(
1200 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1201 serialize_with = "uv_pep508::marker::ser::serialize",
1202 default
1203 )]
1204 marker: MarkerTree,
1205 extra: Option<ExtraName>,
1206 group: Option<GroupName>,
1207 },
1208}
1209
1210impl<'de> Deserialize<'de> for Source {
1213 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1214 where
1215 D: Deserializer<'de>,
1216 {
1217 #[derive(Deserialize, Debug, Clone)]
1218 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1219 struct CatchAll {
1220 git: Option<DisplaySafeUrl>,
1221 subdirectory: Option<PortablePathBuf>,
1222 rev: Option<String>,
1223 tag: Option<String>,
1224 branch: Option<String>,
1225 lfs: Option<bool>,
1226 url: Option<DisplaySafeUrl>,
1227 path: Option<PortablePathBuf>,
1228 editable: Option<bool>,
1229 package: Option<bool>,
1230 index: Option<IndexName>,
1231 workspace: Option<bool>,
1232 #[serde(
1233 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1234 serialize_with = "uv_pep508::marker::ser::serialize",
1235 default
1236 )]
1237 marker: MarkerTree,
1238 extra: Option<ExtraName>,
1239 group: Option<GroupName>,
1240 }
1241
1242 let CatchAll {
1244 git,
1245 subdirectory,
1246 rev,
1247 tag,
1248 branch,
1249 lfs,
1250 url,
1251 path,
1252 editable,
1253 package,
1254 index,
1255 workspace,
1256 marker,
1257 extra,
1258 group,
1259 } = CatchAll::deserialize(deserializer)?;
1260
1261 if extra.is_some() && group.is_some() {
1263 return Err(serde::de::Error::custom(
1264 "cannot specify both `extra` and `group`",
1265 ));
1266 }
1267
1268 if let Some(git) = git {
1270 if index.is_some() {
1271 return Err(serde::de::Error::custom(
1272 "cannot specify both `git` and `index`",
1273 ));
1274 }
1275 if workspace.is_some() {
1276 return Err(serde::de::Error::custom(
1277 "cannot specify both `git` and `workspace`",
1278 ));
1279 }
1280 if path.is_some() {
1281 return Err(serde::de::Error::custom(
1282 "cannot specify both `git` and `path`",
1283 ));
1284 }
1285 if url.is_some() {
1286 return Err(serde::de::Error::custom(
1287 "cannot specify both `git` and `url`",
1288 ));
1289 }
1290 if editable.is_some() {
1291 return Err(serde::de::Error::custom(
1292 "cannot specify both `git` and `editable`",
1293 ));
1294 }
1295 if package.is_some() {
1296 return Err(serde::de::Error::custom(
1297 "cannot specify both `git` and `package`",
1298 ));
1299 }
1300
1301 match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1303 (None, None, None) => {}
1304 (Some(_), None, None) => {}
1305 (None, Some(_), None) => {}
1306 (None, None, Some(_)) => {}
1307 _ => {
1308 return Err(serde::de::Error::custom(
1309 "expected at most one of `rev`, `tag`, or `branch`",
1310 ));
1311 }
1312 }
1313
1314 let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1316 DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1317 } else {
1318 git
1319 };
1320
1321 return Ok(Self::Git {
1322 git,
1323 subdirectory,
1324 rev,
1325 tag,
1326 branch,
1327 lfs,
1328 marker,
1329 extra,
1330 group,
1331 });
1332 }
1333
1334 if let Some(url) = url {
1336 if index.is_some() {
1337 return Err(serde::de::Error::custom(
1338 "cannot specify both `url` and `index`",
1339 ));
1340 }
1341 if workspace.is_some() {
1342 return Err(serde::de::Error::custom(
1343 "cannot specify both `url` and `workspace`",
1344 ));
1345 }
1346 if path.is_some() {
1347 return Err(serde::de::Error::custom(
1348 "cannot specify both `url` and `path`",
1349 ));
1350 }
1351 if git.is_some() {
1352 return Err(serde::de::Error::custom(
1353 "cannot specify both `url` and `git`",
1354 ));
1355 }
1356 if rev.is_some() {
1357 return Err(serde::de::Error::custom(
1358 "cannot specify both `url` and `rev`",
1359 ));
1360 }
1361 if tag.is_some() {
1362 return Err(serde::de::Error::custom(
1363 "cannot specify both `url` and `tag`",
1364 ));
1365 }
1366 if branch.is_some() {
1367 return Err(serde::de::Error::custom(
1368 "cannot specify both `url` and `branch`",
1369 ));
1370 }
1371 if editable.is_some() {
1372 return Err(serde::de::Error::custom(
1373 "cannot specify both `url` and `editable`",
1374 ));
1375 }
1376 if package.is_some() {
1377 return Err(serde::de::Error::custom(
1378 "cannot specify both `url` and `package`",
1379 ));
1380 }
1381
1382 return Ok(Self::Url {
1383 url,
1384 subdirectory,
1385 marker,
1386 extra,
1387 group,
1388 });
1389 }
1390
1391 if let Some(path) = path {
1393 if index.is_some() {
1394 return Err(serde::de::Error::custom(
1395 "cannot specify both `path` and `index`",
1396 ));
1397 }
1398 if workspace.is_some() {
1399 return Err(serde::de::Error::custom(
1400 "cannot specify both `path` and `workspace`",
1401 ));
1402 }
1403 if git.is_some() {
1404 return Err(serde::de::Error::custom(
1405 "cannot specify both `path` and `git`",
1406 ));
1407 }
1408 if url.is_some() {
1409 return Err(serde::de::Error::custom(
1410 "cannot specify both `path` and `url`",
1411 ));
1412 }
1413 if rev.is_some() {
1414 return Err(serde::de::Error::custom(
1415 "cannot specify both `path` and `rev`",
1416 ));
1417 }
1418 if tag.is_some() {
1419 return Err(serde::de::Error::custom(
1420 "cannot specify both `path` and `tag`",
1421 ));
1422 }
1423 if branch.is_some() {
1424 return Err(serde::de::Error::custom(
1425 "cannot specify both `path` and `branch`",
1426 ));
1427 }
1428
1429 if editable == Some(true) && package == Some(false) {
1431 return Err(serde::de::Error::custom(
1432 "cannot specify both `editable = true` and `package = false`",
1433 ));
1434 }
1435
1436 return Ok(Self::Path {
1437 path,
1438 editable,
1439 package,
1440 marker,
1441 extra,
1442 group,
1443 });
1444 }
1445
1446 if let Some(index) = index {
1448 if workspace.is_some() {
1449 return Err(serde::de::Error::custom(
1450 "cannot specify both `index` and `workspace`",
1451 ));
1452 }
1453 if git.is_some() {
1454 return Err(serde::de::Error::custom(
1455 "cannot specify both `index` and `git`",
1456 ));
1457 }
1458 if url.is_some() {
1459 return Err(serde::de::Error::custom(
1460 "cannot specify both `index` and `url`",
1461 ));
1462 }
1463 if path.is_some() {
1464 return Err(serde::de::Error::custom(
1465 "cannot specify both `index` and `path`",
1466 ));
1467 }
1468 if rev.is_some() {
1469 return Err(serde::de::Error::custom(
1470 "cannot specify both `index` and `rev`",
1471 ));
1472 }
1473 if tag.is_some() {
1474 return Err(serde::de::Error::custom(
1475 "cannot specify both `index` and `tag`",
1476 ));
1477 }
1478 if branch.is_some() {
1479 return Err(serde::de::Error::custom(
1480 "cannot specify both `index` and `branch`",
1481 ));
1482 }
1483 if editable.is_some() {
1484 return Err(serde::de::Error::custom(
1485 "cannot specify both `index` and `editable`",
1486 ));
1487 }
1488 if package.is_some() {
1489 return Err(serde::de::Error::custom(
1490 "cannot specify both `index` and `package`",
1491 ));
1492 }
1493
1494 return Ok(Self::Registry {
1495 index,
1496 marker,
1497 extra,
1498 group,
1499 });
1500 }
1501
1502 if let Some(workspace) = workspace {
1504 if index.is_some() {
1505 return Err(serde::de::Error::custom(
1506 "cannot specify both `workspace` and `index`",
1507 ));
1508 }
1509 if git.is_some() {
1510 return Err(serde::de::Error::custom(
1511 "cannot specify both `workspace` and `git`",
1512 ));
1513 }
1514 if url.is_some() {
1515 return Err(serde::de::Error::custom(
1516 "cannot specify both `workspace` and `url`",
1517 ));
1518 }
1519 if path.is_some() {
1520 return Err(serde::de::Error::custom(
1521 "cannot specify both `workspace` and `path`",
1522 ));
1523 }
1524 if rev.is_some() {
1525 return Err(serde::de::Error::custom(
1526 "cannot specify both `workspace` and `rev`",
1527 ));
1528 }
1529 if tag.is_some() {
1530 return Err(serde::de::Error::custom(
1531 "cannot specify both `workspace` and `tag`",
1532 ));
1533 }
1534 if branch.is_some() {
1535 return Err(serde::de::Error::custom(
1536 "cannot specify both `workspace` and `branch`",
1537 ));
1538 }
1539 if package.is_some() {
1540 return Err(serde::de::Error::custom(
1541 "cannot specify both `workspace` and `package`",
1542 ));
1543 }
1544
1545 return Ok(Self::Workspace {
1546 workspace,
1547 editable,
1548 marker,
1549 extra,
1550 group,
1551 });
1552 }
1553
1554 Err(serde::de::Error::custom(
1556 "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1557 ))
1558 }
1559}
1560
1561#[derive(Error, Debug)]
1562pub enum SourceError {
1563 #[error("Failed to resolve Git reference: `{0}`")]
1564 UnresolvedReference(String),
1565 #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1566 WorkspacePackageGit(String),
1567 #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1568 WorkspacePackageUrl(String),
1569 #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1570 WorkspacePackageFile(String),
1571 #[error(
1572 "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1573 )]
1574 UnusedRev(String, String),
1575 #[error(
1576 "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1577 )]
1578 UnusedTag(String, String),
1579 #[error(
1580 "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1581 )]
1582 UnusedBranch(String, String),
1583 #[error(
1584 "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1585 )]
1586 UnusedLfs(String),
1587 #[error(
1588 "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1589 )]
1590 UnusedEditable(String),
1591 #[error("Failed to resolve absolute path")]
1592 Absolute(#[from] std::io::Error),
1593 #[error("Path contains invalid characters: `{}`", _0.display())]
1594 NonUtf8Path(PathBuf),
1595 #[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()
1596 )]
1597 OverlappingMarkers(String, String, String),
1598 #[error(
1599 "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1600 )]
1601 MissingMarkers,
1602 #[error("Must provide at least one source")]
1603 EmptySources,
1604}
1605
1606impl Source {
1607 pub fn from_requirement(
1608 name: &PackageName,
1609 source: RequirementSource,
1610 workspace: bool,
1611 editable: Option<bool>,
1612 index: Option<IndexName>,
1613 rev: Option<String>,
1614 tag: Option<String>,
1615 branch: Option<String>,
1616 lfs: GitLfsSetting,
1617 root: &Path,
1618 existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1619 ) -> Result<Option<Self>, SourceError> {
1620 if !matches!(source, RequirementSource::Git { .. })
1622 && (branch.is_some()
1623 || tag.is_some()
1624 || rev.is_some()
1625 || matches!(lfs, GitLfsSetting::Enabled { .. }))
1626 {
1627 if let Some(sources) = existing_sources {
1628 if let Some(package_sources) = sources.get(name) {
1629 for existing_source in package_sources.iter() {
1630 if let Self::Git {
1631 git,
1632 subdirectory,
1633 marker,
1634 extra,
1635 group,
1636 ..
1637 } = existing_source
1638 {
1639 return Ok(Some(Self::Git {
1640 git: git.clone(),
1641 subdirectory: subdirectory.clone(),
1642 rev,
1643 tag,
1644 branch,
1645 lfs: lfs.into(),
1646 marker: *marker,
1647 extra: extra.clone(),
1648 group: group.clone(),
1649 }));
1650 }
1651 }
1652 }
1653 }
1654 if let Some(rev) = rev {
1655 return Err(SourceError::UnusedRev(name.to_string(), rev));
1656 }
1657 if let Some(tag) = tag {
1658 return Err(SourceError::UnusedTag(name.to_string(), tag));
1659 }
1660 if let Some(branch) = branch {
1661 return Err(SourceError::UnusedBranch(name.to_string(), branch));
1662 }
1663 if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1664 return Err(SourceError::UnusedLfs(name.to_string()));
1665 }
1666 }
1667
1668 if !workspace {
1670 if !matches!(source, RequirementSource::Directory { .. }) {
1671 if editable == Some(true) {
1672 return Err(SourceError::UnusedEditable(name.to_string()));
1673 }
1674 }
1675 }
1676
1677 if workspace {
1679 return match source {
1680 RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1681 Ok(Some(Self::Workspace {
1682 workspace: true,
1683 editable,
1684 marker: MarkerTree::TRUE,
1685 extra: None,
1686 group: None,
1687 }))
1688 }
1689 RequirementSource::Url { .. } => {
1690 Err(SourceError::WorkspacePackageUrl(name.to_string()))
1691 }
1692 RequirementSource::Git { .. } => {
1693 Err(SourceError::WorkspacePackageGit(name.to_string()))
1694 }
1695 RequirementSource::Path { .. } => {
1696 Err(SourceError::WorkspacePackageFile(name.to_string()))
1697 }
1698 };
1699 }
1700
1701 let source = match source {
1702 RequirementSource::Registry { index: Some(_), .. } => {
1703 return Ok(None);
1704 }
1705 RequirementSource::Registry { index: None, .. } => {
1706 if let Some(index) = index {
1707 Self::Registry {
1708 index,
1709 marker: MarkerTree::TRUE,
1710 extra: None,
1711 group: None,
1712 }
1713 } else {
1714 return Ok(None);
1715 }
1716 }
1717 RequirementSource::Path { install_path, .. } => Self::Path {
1718 editable: None,
1719 package: None,
1720 path: PortablePathBuf::from(
1721 relative_to(&install_path, root)
1722 .or_else(|_| std::path::absolute(&install_path))
1723 .map_err(SourceError::Absolute)?
1724 .into_boxed_path(),
1725 ),
1726 marker: MarkerTree::TRUE,
1727 extra: None,
1728 group: None,
1729 },
1730 RequirementSource::Directory {
1731 install_path,
1732 editable: is_editable,
1733 ..
1734 } => Self::Path {
1735 editable: editable.or(is_editable),
1736 package: None,
1737 path: PortablePathBuf::from(
1738 relative_to(&install_path, root)
1739 .or_else(|_| std::path::absolute(&install_path))
1740 .map_err(SourceError::Absolute)?
1741 .into_boxed_path(),
1742 ),
1743 marker: MarkerTree::TRUE,
1744 extra: None,
1745 group: None,
1746 },
1747 RequirementSource::Url {
1748 location,
1749 subdirectory,
1750 ..
1751 } => Self::Url {
1752 url: location,
1753 subdirectory: subdirectory.map(PortablePathBuf::from),
1754 marker: MarkerTree::TRUE,
1755 extra: None,
1756 group: None,
1757 },
1758 RequirementSource::Git {
1759 git, subdirectory, ..
1760 } => {
1761 if rev.is_none() && tag.is_none() && branch.is_none() {
1762 let rev = match git.reference() {
1763 GitReference::Branch(rev) => Some(rev),
1764 GitReference::Tag(rev) => Some(rev),
1765 GitReference::BranchOrTag(rev) => Some(rev),
1766 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1767 GitReference::NamedRef(rev) => Some(rev),
1768 GitReference::DefaultBranch => None,
1769 };
1770 Self::Git {
1771 rev: rev.cloned(),
1772 tag,
1773 branch,
1774 lfs: lfs.into(),
1775 git: git.repository().clone(),
1776 subdirectory: subdirectory.map(PortablePathBuf::from),
1777 marker: MarkerTree::TRUE,
1778 extra: None,
1779 group: None,
1780 }
1781 } else {
1782 Self::Git {
1783 rev,
1784 tag,
1785 branch,
1786 lfs: lfs.into(),
1787 git: git.repository().clone(),
1788 subdirectory: subdirectory.map(PortablePathBuf::from),
1789 marker: MarkerTree::TRUE,
1790 extra: None,
1791 group: None,
1792 }
1793 }
1794 }
1795 };
1796
1797 Ok(Some(source))
1798 }
1799
1800 pub fn marker(&self) -> MarkerTree {
1802 match self {
1803 Self::Git { marker, .. } => *marker,
1804 Self::Url { marker, .. } => *marker,
1805 Self::Path { marker, .. } => *marker,
1806 Self::Registry { marker, .. } => *marker,
1807 Self::Workspace { marker, .. } => *marker,
1808 }
1809 }
1810
1811 pub fn extra(&self) -> Option<&ExtraName> {
1813 match self {
1814 Self::Git { extra, .. } => extra.as_ref(),
1815 Self::Url { extra, .. } => extra.as_ref(),
1816 Self::Path { extra, .. } => extra.as_ref(),
1817 Self::Registry { extra, .. } => extra.as_ref(),
1818 Self::Workspace { extra, .. } => extra.as_ref(),
1819 }
1820 }
1821
1822 pub fn group(&self) -> Option<&GroupName> {
1824 match self {
1825 Self::Git { group, .. } => group.as_ref(),
1826 Self::Url { group, .. } => group.as_ref(),
1827 Self::Path { group, .. } => group.as_ref(),
1828 Self::Registry { group, .. } => group.as_ref(),
1829 Self::Workspace { group, .. } => group.as_ref(),
1830 }
1831 }
1832}
1833
1834#[derive(Debug, Clone, PartialEq, Eq)]
1836pub enum DependencyType {
1837 Production,
1839 Dev,
1841 Optional(ExtraName),
1843 Group(GroupName),
1845}
1846
1847#[derive(Debug, Clone, PartialEq, Eq)]
1848#[cfg_attr(test, derive(Serialize))]
1849pub struct BuildBackendSettingsSchema;
1850
1851impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1852 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1853 where
1854 D: Deserializer<'de>,
1855 {
1856 Ok(Self)
1857 }
1858}
1859
1860#[cfg(feature = "schemars")]
1861impl schemars::JsonSchema for BuildBackendSettingsSchema {
1862 fn schema_name() -> Cow<'static, str> {
1863 BuildBackendSettings::schema_name()
1864 }
1865
1866 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1867 BuildBackendSettings::json_schema(generator)
1868 }
1869}
1870
1871impl OptionsMetadata for BuildBackendSettingsSchema {
1872 fn record(visit: &mut dyn Visit) {
1873 BuildBackendSettings::record(visit);
1874 }
1875
1876 fn documentation() -> Option<&'static str> {
1877 BuildBackendSettings::documentation()
1878 }
1879
1880 fn metadata() -> OptionSet
1881 where
1882 Self: Sized + 'static,
1883 {
1884 BuildBackendSettings::metadata()
1885 }
1886}