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::SeqAccess;
21use serde::{Deserialize, Deserializer, Serialize};
22use thiserror::Error;
23use tracing::instrument;
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 Toml(#[from] toml::de::Error),
43 #[error(
44 "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
45 )]
46 MissingName,
47 #[error(
48 "`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list"
49 )]
50 MissingVersion,
51}
52
53fn deserialize_unique_map<'de, D, K, V, F>(
55 deserializer: D,
56 error_msg: F,
57) -> Result<BTreeMap<K, V>, D::Error>
58where
59 D: Deserializer<'de>,
60 K: Deserialize<'de> + Ord + std::fmt::Display,
61 V: Deserialize<'de>,
62 F: FnOnce(&K) -> String,
63{
64 struct Visitor<K, V, F>(F, std::marker::PhantomData<(K, V)>);
65
66 impl<'de, K, V, F> serde::de::Visitor<'de> for Visitor<K, V, F>
67 where
68 K: Deserialize<'de> + Ord + std::fmt::Display,
69 V: Deserialize<'de>,
70 F: FnOnce(&K) -> String,
71 {
72 type Value = BTreeMap<K, V>;
73
74 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
75 formatter.write_str("a map with unique keys")
76 }
77
78 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
79 where
80 M: serde::de::MapAccess<'de>,
81 {
82 use std::collections::btree_map::Entry;
83
84 let mut map = BTreeMap::new();
85 while let Some((key, value)) = access.next_entry::<K, V>()? {
86 match map.entry(key) {
87 Entry::Occupied(entry) => {
88 return Err(serde::de::Error::custom((self.0)(entry.key())));
89 }
90 Entry::Vacant(entry) => {
91 entry.insert(value);
92 }
93 }
94 }
95 Ok(map)
96 }
97 }
98
99 deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData))
100}
101
102#[derive(Deserialize, Debug, Clone)]
104#[cfg_attr(test, derive(Serialize))]
105#[serde(rename_all = "kebab-case")]
106pub struct PyProjectToml {
107 pub project: Option<Project>,
109 pub tool: Option<Tool>,
111 pub dependency_groups: Option<DependencyGroups>,
113 #[serde(skip)]
115 pub raw: String,
116
117 #[serde(default, skip_serializing)]
119 pub build_system: Option<serde::de::IgnoredAny>,
120}
121
122impl PyProjectToml {
123 #[instrument("toml::from_str workspace", skip_all, fields(path = %_path.as_ref().display()))]
125 pub fn from_string(raw: String, _path: impl AsRef<Path>) -> Result<Self, PyprojectTomlError> {
126 let pyproject = toml::from_str(&raw).map_err(PyprojectTomlError::Toml)?;
127 Ok(Self { raw, ..pyproject })
128 }
129
130 pub fn is_package(&self, require_build_system: bool) -> bool {
133 if let Some(is_package) = self.tool_uv_package() {
135 return is_package;
136 }
137
138 self.build_system.is_some() || !require_build_system
140 }
141
142 fn tool_uv_package(&self) -> Option<bool> {
144 self.tool
145 .as_ref()
146 .and_then(|tool| tool.uv.as_ref())
147 .and_then(|uv| uv.package)
148 }
149
150 pub fn is_dynamic(&self) -> bool {
152 self.project
153 .as_ref()
154 .is_some_and(|project| project.version.is_none())
155 }
156
157 pub fn has_scripts(&self) -> bool {
159 if let Some(ref project) = self.project {
160 project.gui_scripts.is_some() || project.scripts.is_some()
161 } else {
162 false
163 }
164 }
165
166 pub fn conflicts(&self) -> Conflicts {
168 let empty = Conflicts::empty();
169 let Some(project) = self.project.as_ref() else {
170 return empty;
171 };
172 let Some(tool) = self.tool.as_ref() else {
173 return empty;
174 };
175 let Some(tooluv) = tool.uv.as_ref() else {
176 return empty;
177 };
178 let Some(conflicting) = tooluv.conflicts.as_ref() else {
179 return empty;
180 };
181 conflicting.to_conflicts_with_package_name(&project.name)
182 }
183}
184
185impl PartialEq for PyProjectToml {
187 fn eq(&self, other: &Self) -> bool {
188 self.project.eq(&other.project) && self.tool.eq(&other.tool)
189 }
190}
191
192impl Eq for PyProjectToml {}
193
194impl AsRef<[u8]> for PyProjectToml {
195 fn as_ref(&self) -> &[u8] {
196 self.raw.as_bytes()
197 }
198}
199
200#[derive(Deserialize, Debug, Clone, PartialEq)]
204#[cfg_attr(test, derive(Serialize))]
205#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
206pub struct Project {
207 pub name: PackageName,
209 pub version: Option<Version>,
211 pub requires_python: Option<VersionSpecifiers>,
213 pub dependencies: Option<Vec<String>>,
215 pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
217
218 #[serde(default, skip_serializing)]
220 pub(crate) gui_scripts: Option<serde::de::IgnoredAny>,
221 #[serde(default, skip_serializing)]
223 pub(crate) scripts: Option<serde::de::IgnoredAny>,
224}
225
226#[derive(Deserialize, Debug)]
227#[serde(rename_all = "kebab-case")]
228struct ProjectWire {
229 name: Option<PackageName>,
230 version: Option<Version>,
231 dynamic: Option<Vec<String>>,
232 requires_python: Option<VersionSpecifiers>,
233 dependencies: Option<Vec<String>>,
234 optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
235 gui_scripts: Option<serde::de::IgnoredAny>,
236 scripts: Option<serde::de::IgnoredAny>,
237}
238
239impl TryFrom<ProjectWire> for Project {
240 type Error = PyprojectTomlError;
241
242 fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
243 let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
245
246 if value.version.is_none()
248 && !value
249 .dynamic
250 .as_ref()
251 .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
252 {
253 return Err(PyprojectTomlError::MissingVersion);
254 }
255
256 Ok(Self {
257 name,
258 version: value.version,
259 requires_python: value.requires_python,
260 dependencies: value.dependencies,
261 optional_dependencies: value.optional_dependencies,
262 gui_scripts: value.gui_scripts,
263 scripts: value.scripts,
264 })
265 }
266}
267
268#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
269#[cfg_attr(test, derive(Serialize))]
270#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
271pub struct Tool {
272 pub uv: Option<ToolUv>,
273}
274
275fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
281where
282 D: Deserializer<'de>,
283{
284 let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
285 if let Some(indexes) = indexes.as_ref() {
286 let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
287 let mut seen_default = false;
288 for index in indexes {
289 if let Some(name) = index.name.as_ref() {
290 if !seen_names.insert(name) {
291 return Err(serde::de::Error::custom(format!(
292 "duplicate index name `{name}`"
293 )));
294 }
295 }
296 if index.default {
297 if seen_default {
298 return Err(serde::de::Error::custom(
299 "found multiple indexes with `default = true`; only one index may be marked as default",
300 ));
301 }
302 seen_default = true;
303 }
304 }
305 }
306 Ok(indexes)
307}
308
309#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
312#[cfg_attr(test, derive(Serialize))]
313#[serde(rename_all = "kebab-case")]
314#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
315pub struct ToolUv {
316 #[option(
324 default = "{}",
325 value_type = "dict",
326 example = r#"
327 [tool.uv.sources]
328 httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
329 pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
330 pydantic = { path = "/path/to/pydantic", editable = true }
331 "#
332 )]
333 pub sources: Option<ToolUvSources>,
334
335 #[option(
363 default = "[]",
364 value_type = "dict",
365 example = r#"
366 [[tool.uv.index]]
367 name = "pytorch"
368 url = "https://download.pytorch.org/whl/cu121"
369 "#
370 )]
371 #[serde(deserialize_with = "deserialize_index_vec", default)]
372 pub index: Option<Vec<Index>>,
373
374 #[option_group]
376 pub workspace: Option<ToolUvWorkspace>,
377
378 #[option(
381 default = r#"true"#,
382 value_type = "bool",
383 example = r#"
384 managed = false
385 "#
386 )]
387 pub managed: Option<bool>,
388
389 #[option(
400 default = r#"true"#,
401 value_type = "bool",
402 example = r#"
403 package = false
404 "#
405 )]
406 pub package: Option<bool>,
407
408 #[option(
412 default = r#"["dev"]"#,
413 value_type = r#"str | list[str]"#,
414 example = r#"
415 default-groups = ["docs"]
416 "#
417 )]
418 pub default_groups: Option<DefaultGroups>,
419
420 #[option(
429 default = "[]",
430 value_type = "dict",
431 example = r#"
432 [tool.uv.dependency-groups]
433 my-group = {requires-python = ">=3.12"}
434 "#
435 )]
436 pub dependency_groups: Option<ToolUvDependencyGroups>,
437
438 #[cfg_attr(
448 feature = "schemars",
449 schemars(
450 with = "Option<Vec<String>>",
451 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
452 )
453 )]
454 #[option(
455 default = "[]",
456 value_type = "list[str]",
457 example = r#"
458 dev-dependencies = ["ruff==0.5.0"]
459 "#
460 )]
461 pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
462
463 #[cfg_attr(
482 feature = "schemars",
483 schemars(
484 with = "Option<Vec<String>>",
485 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
486 )
487 )]
488 #[option(
489 default = "[]",
490 value_type = "list[str]",
491 example = r#"
492 # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
493 # a different version.
494 override-dependencies = ["werkzeug==2.3.0"]
495 "#
496 )]
497 pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
498
499 #[cfg_attr(
514 feature = "schemars",
515 schemars(
516 with = "Option<Vec<String>>",
517 description = "Package names to exclude, e.g., `werkzeug`, `numpy`."
518 )
519 )]
520 #[option(
521 default = "[]",
522 value_type = "list[str]",
523 example = r#"
524 # Exclude Werkzeug from being installed, even if transitive dependencies request it.
525 exclude-dependencies = ["werkzeug"]
526 "#
527 )]
528 pub exclude_dependencies: Option<Vec<PackageName>>,
529
530 #[cfg_attr(
544 feature = "schemars",
545 schemars(
546 with = "Option<Vec<String>>",
547 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
548 )
549 )]
550 #[option(
551 default = "[]",
552 value_type = "list[str]",
553 example = r#"
554 # Ensure that the grpcio version is always less than 1.65, if it's requested by a
555 # direct or transitive dependency.
556 constraint-dependencies = ["grpcio<1.65"]
557 "#
558 )]
559 pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
560
561 #[cfg_attr(
575 feature = "schemars",
576 schemars(
577 with = "Option<Vec<String>>",
578 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
579 )
580 )]
581 #[option(
582 default = "[]",
583 value_type = "list[str]",
584 example = r#"
585 # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
586 # on setuptools.
587 build-constraint-dependencies = ["setuptools==60.0.0"]
588 "#
589 )]
590 pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
591
592 #[cfg_attr(
601 feature = "schemars",
602 schemars(
603 with = "Option<Vec<String>>",
604 description = "A list of environment markers, e.g., `python_version >= '3.6'`."
605 )
606 )]
607 #[option(
608 default = "[]",
609 value_type = "str | list[str]",
610 example = r#"
611 # Resolve for macOS, but not for Linux or Windows.
612 environments = ["sys_platform == 'darwin'"]
613 "#
614 )]
615 pub environments: Option<SupportedEnvironments>,
616
617 #[cfg_attr(
637 feature = "schemars",
638 schemars(
639 with = "Option<Vec<String>>",
640 description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
641 )
642 )]
643 #[option(
644 default = "[]",
645 value_type = "str | list[str]",
646 example = r#"
647 # Require that the package is available on the following platforms:
648 required-environments = [
649 # macOS on Apple Silicon (ARM)
650 "sys_platform == 'darwin' and platform_machine == 'arm64'",
651 # Linux on x86_64 (Intel/AMD)
652 "sys_platform == 'linux' and platform_machine == 'x86_64'",
653 # Windows on x86_64 (Intel/AMD)
654 "sys_platform == 'win32' and platform_machine == 'AMD64'",
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}