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 rustc_hash::{FxBuildHasher, FxHashSet};
19use serde::de::SeqAccess;
20use serde::{Deserialize, Deserializer, Serialize};
21use thiserror::Error;
22use tracing::instrument;
23use uv_build_backend::BuildBackendSettings;
24use uv_configuration::GitLfsSetting;
25use uv_distribution_types::{Index, IndexName, RequirementSource};
26use uv_fs::{PortablePathBuf, relative_to};
27use uv_git_types::GitReference;
28use uv_macros::OptionsMetadata;
29use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName};
30use uv_options_metadata::{OptionSet, OptionsMetadata, Visit};
31use uv_pep440::{Version, VersionSpecifiers};
32use uv_pep508::MarkerTree;
33use uv_pypi_types::{
34 Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl,
35};
36use uv_redacted::DisplaySafeUrl;
37
38#[derive(Error, Debug)]
39pub enum PyprojectTomlError {
40 #[error(transparent)]
41 Toml(#[from] toml::de::Error),
42 #[error("Failed to parse `tool.uv.sources`")]
43 Source(
44 #[from]
45 #[source]
46 SourceError,
47 ),
48 #[error(
49 "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
50 )]
51 MissingName,
52 #[error(
53 "`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list"
54 )]
55 MissingVersion,
56}
57
58fn deserialize_unique_map<'de, D, K, V, F>(
60 deserializer: D,
61 error_msg: F,
62) -> Result<BTreeMap<K, V>, D::Error>
63where
64 D: Deserializer<'de>,
65 K: Deserialize<'de> + Ord + std::fmt::Display,
66 V: Deserialize<'de>,
67 F: FnOnce(&K) -> String,
68{
69 struct Visitor<K, V, F>(F, std::marker::PhantomData<(K, V)>);
70
71 impl<'de, K, V, F> serde::de::Visitor<'de> for Visitor<K, V, F>
72 where
73 K: Deserialize<'de> + Ord + std::fmt::Display,
74 V: Deserialize<'de>,
75 F: FnOnce(&K) -> String,
76 {
77 type Value = BTreeMap<K, V>;
78
79 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
80 formatter.write_str("a map with unique keys")
81 }
82
83 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
84 where
85 M: serde::de::MapAccess<'de>,
86 {
87 use std::collections::btree_map::Entry;
88
89 let mut map = BTreeMap::new();
90 while let Some((key, value)) = access.next_entry::<K, V>()? {
91 match map.entry(key) {
92 Entry::Occupied(entry) => {
93 return Err(serde::de::Error::custom((self.0)(entry.key())));
94 }
95 Entry::Vacant(entry) => {
96 entry.insert(value);
97 }
98 }
99 }
100 Ok(map)
101 }
102 }
103
104 deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData))
105}
106
107#[derive(Deserialize, Debug, Clone)]
109#[cfg_attr(test, derive(Serialize))]
110#[serde(rename_all = "kebab-case")]
111pub struct PyProjectToml {
112 pub project: Option<Project>,
114 pub tool: Option<Tool>,
116 pub dependency_groups: Option<DependencyGroups>,
118 #[serde(skip)]
120 pub raw: String,
121
122 #[serde(default, skip_serializing)]
124 build_system: Option<serde::de::IgnoredAny>,
125}
126
127impl PyProjectToml {
128 #[instrument("toml::from_str workspace", skip_all, fields(path = %_path.as_ref().display()))]
130 pub fn from_string(raw: String, _path: impl AsRef<Path>) -> Result<Self, PyprojectTomlError> {
131 let sources_wire =
132 toml::from_str::<PyProjectTomlSourcesWire>(&raw).map_err(PyprojectTomlError::Toml)?;
133 let sources = sources_wire
134 .tool
135 .and_then(|tool| tool.uv)
136 .and_then(|uv| uv.sources)
137 .map(ToolUvSources::try_from)
138 .transpose()?;
139
140 let mut pyproject: Self = toml::from_str(&raw).map_err(PyprojectTomlError::Toml)?;
141 if let Some(sources) = sources {
142 let tool_uv = pyproject
143 .tool
144 .as_mut()
145 .and_then(|tool| tool.uv.as_mut())
146 .expect("tool.uv must exist when tool.uv.sources is present");
147 tool_uv.sources = Some(sources);
148 }
149
150 Ok(Self { raw, ..pyproject })
151 }
152
153 pub fn is_package(&self, require_build_system: bool) -> bool {
156 if let Some(is_package) = self.tool_uv_package() {
158 return is_package;
159 }
160
161 self.build_system.is_some() || !require_build_system
163 }
164
165 fn tool_uv_package(&self) -> Option<bool> {
167 self.tool
168 .as_ref()
169 .and_then(|tool| tool.uv.as_ref())
170 .and_then(|uv| uv.package)
171 }
172
173 pub fn has_scripts(&self) -> bool {
175 if let Some(ref project) = self.project {
176 project.gui_scripts.is_some() || project.scripts.is_some()
177 } else {
178 false
179 }
180 }
181
182 pub(crate) fn conflicts(&self) -> Conflicts {
184 let empty = Conflicts::empty();
185 let Some(project) = self.project.as_ref() else {
186 return empty;
187 };
188 let Some(tool) = self.tool.as_ref() else {
189 return empty;
190 };
191 let Some(tooluv) = tool.uv.as_ref() else {
192 return empty;
193 };
194 let Some(conflicting) = tooluv.conflicts.as_ref() else {
195 return empty;
196 };
197 conflicting.to_conflicts_with_package_name(&project.name)
198 }
199}
200
201impl PartialEq for PyProjectToml {
203 fn eq(&self, other: &Self) -> bool {
204 self.project.eq(&other.project) && self.tool.eq(&other.tool)
205 }
206}
207
208impl Eq for PyProjectToml {}
209
210impl AsRef<[u8]> for PyProjectToml {
211 fn as_ref(&self) -> &[u8] {
212 self.raw.as_bytes()
213 }
214}
215
216#[derive(Deserialize, Debug, Clone, PartialEq)]
220#[cfg_attr(test, derive(Serialize))]
221#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
222pub struct Project {
223 pub name: PackageName,
225 version: Option<Version>,
227 pub(crate) requires_python: Option<VersionSpecifiers>,
229 pub dependencies: Option<Vec<String>>,
231 pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
233
234 #[serde(default, skip_serializing)]
236 gui_scripts: Option<serde::de::IgnoredAny>,
237 #[serde(default, skip_serializing)]
239 scripts: Option<serde::de::IgnoredAny>,
240}
241
242#[derive(Deserialize, Debug)]
243#[serde(rename_all = "kebab-case")]
244struct ProjectWire {
245 name: Option<PackageName>,
246 version: Option<Version>,
247 dynamic: Option<Vec<String>>,
248 requires_python: Option<VersionSpecifiers>,
249 dependencies: Option<Vec<String>>,
250 optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
251 gui_scripts: Option<serde::de::IgnoredAny>,
252 scripts: Option<serde::de::IgnoredAny>,
253}
254
255impl TryFrom<ProjectWire> for Project {
256 type Error = PyprojectTomlError;
257
258 fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
259 let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
261
262 if value.version.is_none()
264 && !value
265 .dynamic
266 .as_ref()
267 .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
268 {
269 return Err(PyprojectTomlError::MissingVersion);
270 }
271
272 Ok(Self {
273 name,
274 version: value.version,
275 requires_python: value.requires_python,
276 dependencies: value.dependencies,
277 optional_dependencies: value.optional_dependencies,
278 gui_scripts: value.gui_scripts,
279 scripts: value.scripts,
280 })
281 }
282}
283
284#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
285#[cfg_attr(test, derive(Serialize))]
286#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
287pub struct Tool {
288 pub uv: Option<ToolUv>,
289}
290
291fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
297where
298 D: Deserializer<'de>,
299{
300 let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
301 if let Some(indexes) = indexes.as_ref() {
302 let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
303 let mut seen_default = false;
304 for index in indexes {
305 if let Some(name) = index.name.as_ref() {
306 if !seen_names.insert(name) {
307 return Err(serde::de::Error::custom(format!(
308 "duplicate index name `{name}`"
309 )));
310 }
311 }
312 if index.default {
313 if seen_default {
314 return Err(serde::de::Error::custom(
315 "found multiple indexes with `default = true`; only one index may be marked as default",
316 ));
317 }
318 seen_default = true;
319 }
320 }
321 }
322 Ok(indexes)
323}
324
325#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
328#[cfg_attr(test, derive(Serialize))]
329#[serde(rename_all = "kebab-case")]
330#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
331pub struct ToolUv {
332 #[option(
340 default = "{}",
341 value_type = "dict",
342 example = r#"
343 [tool.uv.sources]
344 httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
345 pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
346 pydantic = { path = "/path/to/pydantic", editable = true }
347 "#
348 )]
349 #[serde(default, deserialize_with = "ignore_tool_uv_sources")]
350 pub sources: Option<ToolUvSources>,
351
352 #[option(
380 default = "[]",
381 value_type = "dict",
382 example = r#"
383 [[tool.uv.index]]
384 name = "pytorch"
385 url = "https://download.pytorch.org/whl/cu130"
386 "#
387 )]
388 #[serde(deserialize_with = "deserialize_index_vec", default)]
389 pub index: Option<Vec<Index>>,
390
391 #[option_group]
393 pub(crate) workspace: Option<ToolUvWorkspace>,
394
395 #[option(
398 default = r#"true"#,
399 value_type = "bool",
400 example = r#"
401 managed = false
402 "#
403 )]
404 pub(crate) managed: Option<bool>,
405
406 #[option(
417 default = r#"true"#,
418 value_type = "bool",
419 example = r#"
420 package = false
421 "#
422 )]
423 package: Option<bool>,
424
425 #[option(
429 default = r#"["dev"]"#,
430 value_type = r#"str | list[str]"#,
431 example = r#"
432 default-groups = ["docs"]
433 "#
434 )]
435 pub default_groups: Option<DefaultGroups>,
436
437 #[option(
446 default = "[]",
447 value_type = "dict",
448 example = r#"
449 [tool.uv.dependency-groups]
450 my-group = {requires-python = ">=3.12"}
451 "#
452 )]
453 pub(crate) dependency_groups: Option<ToolUvDependencyGroups>,
454
455 #[cfg_attr(
465 feature = "schemars",
466 schemars(
467 with = "Option<Vec<String>>",
468 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
469 )
470 )]
471 #[option(
472 default = "[]",
473 value_type = "list[str]",
474 example = r#"
475 dev-dependencies = ["ruff==0.5.0"]
476 "#
477 )]
478 pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
479
480 #[cfg_attr(
499 feature = "schemars",
500 schemars(
501 with = "Option<Vec<String>>",
502 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
503 )
504 )]
505 #[option(
506 default = "[]",
507 value_type = "list[str]",
508 example = r#"
509 # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
510 # a different version.
511 override-dependencies = ["werkzeug==2.3.0"]
512 "#
513 )]
514 pub(crate) override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
515
516 #[cfg_attr(
531 feature = "schemars",
532 schemars(
533 with = "Option<Vec<String>>",
534 description = "Package names to exclude, e.g., `werkzeug`, `numpy`."
535 )
536 )]
537 #[option(
538 default = "[]",
539 value_type = "list[str]",
540 example = r#"
541 # Exclude Werkzeug from being installed, even if transitive dependencies request it.
542 exclude-dependencies = ["werkzeug"]
543 "#
544 )]
545 pub(crate) exclude_dependencies: Option<Vec<PackageName>>,
546
547 #[cfg_attr(
561 feature = "schemars",
562 schemars(
563 with = "Option<Vec<String>>",
564 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
565 )
566 )]
567 #[option(
568 default = "[]",
569 value_type = "list[str]",
570 example = r#"
571 # Ensure that the grpcio version is always less than 1.65, if it's requested by a
572 # direct or transitive dependency.
573 constraint-dependencies = ["grpcio<1.65"]
574 "#
575 )]
576 pub(crate) constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
577
578 #[cfg_attr(
592 feature = "schemars",
593 schemars(
594 with = "Option<Vec<String>>",
595 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
596 )
597 )]
598 #[option(
599 default = "[]",
600 value_type = "list[str]",
601 example = r#"
602 # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
603 # on setuptools.
604 build-constraint-dependencies = ["setuptools==60.0.0"]
605 "#
606 )]
607 pub(crate) build_constraint_dependencies:
608 Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
609
610 #[cfg_attr(
619 feature = "schemars",
620 schemars(
621 with = "Option<Vec<String>>",
622 description = "A list of environment markers, e.g., `python_version >= '3.6'`."
623 )
624 )]
625 #[option(
626 default = "[]",
627 value_type = "str | list[str]",
628 example = r#"
629 # Resolve for macOS, but not for Linux or Windows.
630 environments = ["sys_platform == 'darwin'"]
631 "#
632 )]
633 pub(crate) environments: Option<SupportedEnvironments>,
634
635 #[cfg_attr(
655 feature = "schemars",
656 schemars(
657 with = "Option<Vec<String>>",
658 description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
659 )
660 )]
661 #[option(
662 default = "[]",
663 value_type = "str | list[str]",
664 example = r#"
665 # Require that the package is available on the following platforms:
666 required-environments = [
667 # macOS on Apple Silicon (ARM)
668 "sys_platform == 'darwin' and platform_machine == 'arm64'",
669 # Linux on x86_64 (Intel/AMD)
670 "sys_platform == 'linux' and platform_machine == 'x86_64'",
671 # Windows on x86_64 (Intel/AMD)
672 "sys_platform == 'win32' and platform_machine == 'AMD64'",
673 ]
674 "#
675 )]
676 pub(crate) required_environments: Option<SupportedEnvironments>,
677
678 #[cfg_attr(
693 feature = "schemars",
694 schemars(description = "A list of sets of conflicting groups or extras.")
695 )]
696 #[option(
697 default = r#"[]"#,
698 value_type = "list[list[dict]]",
699 example = r#"
700 # Require that `package[extra1]` and `package[extra2]` are resolved
701 # in different forks so that they cannot conflict with one another.
702 conflicts = [
703 [
704 { extra = "extra1" },
705 { extra = "extra2" },
706 ]
707 ]
708
709 # Require that the dependency groups `group1` and `group2`
710 # are resolved in different forks so that they cannot conflict
711 # with one another.
712 conflicts = [
713 [
714 { group = "group1" },
715 { group = "group2" },
716 ]
717 ]
718 "#
719 )]
720 pub(crate) conflicts: Option<SchemaConflicts>,
721
722 #[option_group]
729 build_backend: Option<BuildBackendSettingsSchema>,
730}
731
732#[derive(Default, Debug, Clone, PartialEq, Eq)]
733#[cfg_attr(test, derive(Serialize))]
734#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
735pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
736
737fn ignore_tool_uv_sources<'de, D>(deserializer: D) -> Result<Option<ToolUvSources>, D::Error>
738where
739 D: Deserializer<'de>,
740{
741 serde::de::IgnoredAny::deserialize(deserializer)?;
742 Ok(None)
743}
744
745#[derive(Deserialize, Debug)]
746#[serde(rename_all = "kebab-case")]
747struct PyProjectTomlSourcesWire {
748 tool: Option<ToolSourcesWire>,
749}
750
751#[derive(Deserialize, Debug)]
752struct ToolSourcesWire {
753 uv: Option<ToolUvSourcesOnlyWire>,
754}
755
756#[derive(Deserialize, Debug)]
757#[serde(rename_all = "kebab-case")]
758struct ToolUvSourcesOnlyWire {
759 sources: Option<ToolUvSourcesWire>,
760}
761
762#[derive(Default, Debug, Clone, PartialEq, Eq)]
763struct ToolUvSourcesWire(BTreeMap<PackageName, SourcesWire>);
764
765impl ToolUvSources {
766 pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
768 &self.0
769 }
770
771 #[must_use]
773 pub(crate) fn into_inner(self) -> BTreeMap<PackageName, Sources> {
774 self.0
775 }
776}
777
778impl TryFrom<ToolUvSourcesWire> for ToolUvSources {
779 type Error = SourceError;
780
781 fn try_from(wire: ToolUvSourcesWire) -> Result<Self, Self::Error> {
782 wire.0
783 .into_iter()
784 .map(|(name, sources)| Sources::try_from(sources).map(|sources| (name, sources)))
785 .collect::<Result<BTreeMap<_, _>, _>>()
786 .map(Self)
787 }
788}
789
790impl<'de> serde::de::Deserialize<'de> for ToolUvSourcesWire {
792 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
793 where
794 D: Deserializer<'de>,
795 {
796 deserialize_unique_map(deserializer, |key: &PackageName| {
797 format!("duplicate sources for package `{key}`")
798 })
799 .map(ToolUvSourcesWire)
800 }
801}
802
803impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
805 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
806 where
807 D: Deserializer<'de>,
808 {
809 deserialize_unique_map(deserializer, |key: &PackageName| {
810 format!("duplicate sources for package `{key}`")
811 })
812 .map(ToolUvSources)
813 }
814}
815
816#[derive(Default, Debug, Clone, PartialEq, Eq)]
817#[cfg_attr(test, derive(Serialize))]
818#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
819pub(crate) struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
820
821impl ToolUvDependencyGroups {
822 pub(crate) fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
824 &self.0
825 }
826}
827
828impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
830 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
831 where
832 D: Deserializer<'de>,
833 {
834 deserialize_unique_map(deserializer, |key: &GroupName| {
835 format!("duplicate settings for dependency group `{key}`")
836 })
837 .map(ToolUvDependencyGroups)
838 }
839}
840
841#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
842#[cfg_attr(test, derive(Serialize))]
843#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
844#[serde(rename_all = "kebab-case")]
845pub(crate) struct DependencyGroupSettings {
846 #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
848 pub(crate) requires_python: Option<VersionSpecifiers>,
849}
850
851#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
852#[serde(untagged, rename_all = "kebab-case")]
853#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
854enum ExtraBuildDependencyWire {
855 Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
856 #[serde(rename_all = "kebab-case")]
857 Annotated {
858 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
859 match_runtime: bool,
860 },
861}
862
863#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
864#[serde(
865 deny_unknown_fields,
866 from = "ExtraBuildDependencyWire",
867 into = "ExtraBuildDependencyWire"
868)]
869#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
870pub struct ExtraBuildDependency {
871 pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
872 pub match_runtime: bool,
873}
874
875impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
876 fn from(value: ExtraBuildDependency) -> Self {
877 value.requirement
878 }
879}
880
881impl From<ExtraBuildDependencyWire> for ExtraBuildDependency {
882 fn from(wire: ExtraBuildDependencyWire) -> Self {
883 match wire {
884 ExtraBuildDependencyWire::Unannotated(requirement) => Self {
885 requirement,
886 match_runtime: false,
887 },
888 ExtraBuildDependencyWire::Annotated {
889 requirement,
890 match_runtime,
891 } => Self {
892 requirement,
893 match_runtime,
894 },
895 }
896 }
897}
898
899impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
900 fn from(item: ExtraBuildDependency) -> Self {
901 Self::Annotated {
902 requirement: item.requirement,
903 match_runtime: item.match_runtime,
904 }
905 }
906}
907
908#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
909#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
910pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
911
912impl std::ops::Deref for ExtraBuildDependencies {
913 type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
914
915 fn deref(&self) -> &Self::Target {
916 &self.0
917 }
918}
919
920impl std::ops::DerefMut for ExtraBuildDependencies {
921 fn deref_mut(&mut self) -> &mut Self::Target {
922 &mut self.0
923 }
924}
925
926impl IntoIterator for ExtraBuildDependencies {
927 type Item = (PackageName, Vec<ExtraBuildDependency>);
928 type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
929
930 fn into_iter(self) -> Self::IntoIter {
931 self.0.into_iter()
932 }
933}
934
935impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
936 fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
937 iter: T,
938 ) -> Self {
939 Self(iter.into_iter().collect())
940 }
941}
942
943impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
945 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
946 where
947 D: Deserializer<'de>,
948 {
949 deserialize_unique_map(deserializer, |key: &PackageName| {
950 format!("duplicate extra-build-dependencies for `{key}`")
951 })
952 .map(ExtraBuildDependencies)
953 }
954}
955
956#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
957#[cfg_attr(test, derive(Serialize))]
958#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
959#[serde(rename_all = "kebab-case", deny_unknown_fields)]
960pub(crate) struct ToolUvWorkspace {
961 #[option(
967 default = "[]",
968 value_type = "list[str]",
969 example = r#"
970 members = ["member1", "path/to/member2", "libs/*"]
971 "#
972 )]
973 pub(crate) members: Option<Vec<SerdePattern>>,
974 #[option(
981 default = "[]",
982 value_type = "list[str]",
983 example = r#"
984 exclude = ["member1", "path/to/member2", "libs/*"]
985 "#
986 )]
987 pub(crate) exclude: Option<Vec<SerdePattern>>,
988}
989
990#[derive(Debug, Clone, PartialEq, Eq)]
992pub(crate) struct SerdePattern(Pattern);
993
994impl serde::ser::Serialize for SerdePattern {
995 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
996 where
997 S: serde::ser::Serializer,
998 {
999 self.0.as_str().serialize(serializer)
1000 }
1001}
1002
1003impl<'de> serde::Deserialize<'de> for SerdePattern {
1004 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1005 struct Visitor;
1006
1007 impl serde::de::Visitor<'_> for Visitor {
1008 type Value = SerdePattern;
1009
1010 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
1011 f.write_str("a string")
1012 }
1013
1014 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
1015 Pattern::from_str(v)
1016 .map(SerdePattern)
1017 .map_err(serde::de::Error::custom)
1018 }
1019 }
1020
1021 deserializer.deserialize_str(Visitor)
1022 }
1023}
1024
1025#[cfg(feature = "schemars")]
1026impl schemars::JsonSchema for SerdePattern {
1027 fn schema_name() -> Cow<'static, str> {
1028 Cow::Borrowed("SerdePattern")
1029 }
1030
1031 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
1032 <String as schemars::JsonSchema>::json_schema(generator)
1033 }
1034}
1035
1036impl Deref for SerdePattern {
1037 type Target = Pattern;
1038
1039 fn deref(&self) -> &Self::Target {
1040 &self.0
1041 }
1042}
1043
1044#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1045#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1046#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
1047pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
1048
1049impl Sources {
1050 pub fn iter(&self) -> impl Iterator<Item = &Source> {
1056 self.0.iter()
1057 }
1058}
1059
1060impl FromIterator<Source> for Sources {
1061 fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
1062 Self(iter.into_iter().collect())
1063 }
1064}
1065
1066impl IntoIterator for Sources {
1067 type Item = Source;
1068 type IntoIter = std::vec::IntoIter<Source>;
1069
1070 fn into_iter(self) -> Self::IntoIter {
1071 self.0.into_iter()
1072 }
1073}
1074
1075#[derive(Debug, Clone, PartialEq, Eq)]
1076#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1077enum SourcesWire {
1078 One(Source),
1079 Many(Vec<Source>),
1080}
1081
1082impl<'de> serde::de::Deserialize<'de> for SourcesWire {
1083 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1084 where
1085 D: Deserializer<'de>,
1086 {
1087 struct Visitor;
1088
1089 impl<'de> serde::de::Visitor<'de> for Visitor {
1090 type Value = SourcesWire;
1091
1092 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1093 formatter.write_str("a single source (as a map) or list of sources")
1094 }
1095
1096 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1097 where
1098 A: SeqAccess<'de>,
1099 {
1100 let sources = serde::de::Deserialize::deserialize(
1101 serde::de::value::SeqAccessDeserializer::new(seq),
1102 )?;
1103 Ok(SourcesWire::Many(sources))
1104 }
1105
1106 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1107 where
1108 M: serde::de::MapAccess<'de>,
1109 {
1110 let source = serde::de::Deserialize::deserialize(
1111 serde::de::value::MapAccessDeserializer::new(&mut map),
1112 )?;
1113 Ok(SourcesWire::One(source))
1114 }
1115 }
1116
1117 deserializer.deserialize_any(Visitor)
1118 }
1119}
1120
1121impl TryFrom<SourcesWire> for Sources {
1122 type Error = SourceError;
1123
1124 fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
1125 match wire {
1126 SourcesWire::One(source) => Ok(Self(vec![source])),
1127 SourcesWire::Many(sources) => {
1128 for [lhs, rhs] in sources.array_windows() {
1129 if lhs.extra() != rhs.extra() {
1130 continue;
1131 }
1132 if lhs.group() != rhs.group() {
1133 continue;
1134 }
1135
1136 let lhs = lhs.marker();
1137 let rhs = rhs.marker();
1138 if !lhs.is_disjoint(rhs) {
1139 let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
1140 return Err(SourceError::MissingMarkers);
1141 };
1142
1143 let Some(right) = rhs.contents().map(|contents| contents.to_string())
1144 else {
1145 return Err(SourceError::MissingMarkers);
1146 };
1147
1148 let mut hint = lhs.negate();
1149 hint.and(rhs);
1150 let hint = hint
1151 .contents()
1152 .map(|contents| contents.to_string())
1153 .unwrap_or_else(|| "true".to_string());
1154
1155 return Err(SourceError::OverlappingMarkers(left, right, hint));
1156 }
1157 }
1158
1159 if sources.is_empty() {
1161 return Err(SourceError::EmptySources);
1162 }
1163
1164 Ok(Self(sources))
1165 }
1166 }
1167 }
1168}
1169
1170#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1172#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1173#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1174pub enum Source {
1175 Git {
1182 git: DisplaySafeUrl,
1184 subdirectory: Option<PortablePathBuf>,
1186 path: Option<PortablePathBuf>,
1188 rev: Option<String>,
1190 tag: Option<String>,
1191 branch: Option<String>,
1192 lfs: Option<bool>,
1194 #[serde(
1195 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1196 serialize_with = "uv_pep508::marker::ser::serialize",
1197 default
1198 )]
1199 marker: MarkerTree,
1200 extra: Option<ExtraName>,
1201 group: Option<GroupName>,
1202 },
1203 Url {
1211 url: DisplaySafeUrl,
1212 subdirectory: Option<PortablePathBuf>,
1215 #[serde(
1216 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1217 serialize_with = "uv_pep508::marker::ser::serialize",
1218 default
1219 )]
1220 marker: MarkerTree,
1221 extra: Option<ExtraName>,
1222 group: Option<GroupName>,
1223 },
1224 Path {
1228 path: PortablePathBuf,
1229 editable: Option<bool>,
1231 package: Option<bool>,
1238 #[serde(
1239 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1240 serialize_with = "uv_pep508::marker::ser::serialize",
1241 default
1242 )]
1243 marker: MarkerTree,
1244 extra: Option<ExtraName>,
1245 group: Option<GroupName>,
1246 },
1247 Registry {
1249 index: IndexName,
1250 #[serde(
1251 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1252 serialize_with = "uv_pep508::marker::ser::serialize",
1253 default
1254 )]
1255 marker: MarkerTree,
1256 extra: Option<ExtraName>,
1257 group: Option<GroupName>,
1258 },
1259 Workspace {
1261 workspace: bool,
1264 editable: Option<bool>,
1266 #[serde(
1267 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1268 serialize_with = "uv_pep508::marker::ser::serialize",
1269 default
1270 )]
1271 marker: MarkerTree,
1272 extra: Option<ExtraName>,
1273 group: Option<GroupName>,
1274 },
1275}
1276
1277impl<'de> Deserialize<'de> for Source {
1280 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1281 where
1282 D: Deserializer<'de>,
1283 {
1284 #[derive(Deserialize, Debug, Clone)]
1285 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1286 struct CatchAll {
1287 git: Option<DisplaySafeUrl>,
1288 subdirectory: Option<PortablePathBuf>,
1289 rev: Option<String>,
1290 tag: Option<String>,
1291 branch: Option<String>,
1292 lfs: Option<bool>,
1293 url: Option<DisplaySafeUrl>,
1294 path: Option<PortablePathBuf>,
1295 editable: Option<bool>,
1296 package: Option<bool>,
1297 index: Option<IndexName>,
1298 workspace: Option<bool>,
1299 #[serde(
1300 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1301 serialize_with = "uv_pep508::marker::ser::serialize",
1302 default
1303 )]
1304 marker: MarkerTree,
1305 extra: Option<ExtraName>,
1306 group: Option<GroupName>,
1307 }
1308
1309 let CatchAll {
1311 git,
1312 subdirectory,
1313 rev,
1314 tag,
1315 branch,
1316 lfs,
1317 url,
1318 path,
1319 editable,
1320 package,
1321 index,
1322 workspace,
1323 marker,
1324 extra,
1325 group,
1326 } = CatchAll::deserialize(deserializer)?;
1327
1328 if extra.is_some() && group.is_some() {
1330 return Err(serde::de::Error::custom(
1331 "cannot specify both `extra` and `group`",
1332 ));
1333 }
1334
1335 if let Some(git) = git {
1337 if index.is_some() {
1338 return Err(serde::de::Error::custom(
1339 "cannot specify both `git` and `index`",
1340 ));
1341 }
1342 if workspace.is_some() {
1343 return Err(serde::de::Error::custom(
1344 "cannot specify both `git` and `workspace`",
1345 ));
1346 }
1347 if url.is_some() {
1348 return Err(serde::de::Error::custom(
1349 "cannot specify both `git` and `url`",
1350 ));
1351 }
1352 if editable.is_some() {
1353 return Err(serde::de::Error::custom(
1354 "cannot specify both `git` and `editable`",
1355 ));
1356 }
1357 if package.is_some() {
1358 return Err(serde::de::Error::custom(
1359 "cannot specify both `git` and `package`",
1360 ));
1361 }
1362 if subdirectory.is_some() && path.is_some() {
1363 return Err(serde::de::Error::custom(
1364 "cannot specify both `subdirectory` and `path`",
1365 ));
1366 }
1367
1368 match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1370 (None, None, None) => {}
1371 (Some(_), None, None) => {}
1372 (None, Some(_), None) => {}
1373 (None, None, Some(_)) => {}
1374 _ => {
1375 return Err(serde::de::Error::custom(
1376 "expected at most one of `rev`, `tag`, or `branch`",
1377 ));
1378 }
1379 }
1380
1381 let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1383 DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1384 } else {
1385 git
1386 };
1387
1388 return Ok(Self::Git {
1389 git,
1390 subdirectory,
1391 path,
1392 rev,
1393 tag,
1394 branch,
1395 lfs,
1396 marker,
1397 extra,
1398 group,
1399 });
1400 }
1401
1402 if let Some(url) = url {
1404 if index.is_some() {
1405 return Err(serde::de::Error::custom(
1406 "cannot specify both `url` and `index`",
1407 ));
1408 }
1409 if workspace.is_some() {
1410 return Err(serde::de::Error::custom(
1411 "cannot specify both `url` and `workspace`",
1412 ));
1413 }
1414 if path.is_some() {
1415 return Err(serde::de::Error::custom(
1416 "cannot specify both `url` and `path`",
1417 ));
1418 }
1419 if git.is_some() {
1420 return Err(serde::de::Error::custom(
1421 "cannot specify both `url` and `git`",
1422 ));
1423 }
1424 if rev.is_some() {
1425 return Err(serde::de::Error::custom(
1426 "cannot specify both `url` and `rev`",
1427 ));
1428 }
1429 if tag.is_some() {
1430 return Err(serde::de::Error::custom(
1431 "cannot specify both `url` and `tag`",
1432 ));
1433 }
1434 if branch.is_some() {
1435 return Err(serde::de::Error::custom(
1436 "cannot specify both `url` and `branch`",
1437 ));
1438 }
1439 if editable.is_some() {
1440 return Err(serde::de::Error::custom(
1441 "cannot specify both `url` and `editable`",
1442 ));
1443 }
1444 if package.is_some() {
1445 return Err(serde::de::Error::custom(
1446 "cannot specify both `url` and `package`",
1447 ));
1448 }
1449
1450 return Ok(Self::Url {
1451 url,
1452 subdirectory,
1453 marker,
1454 extra,
1455 group,
1456 });
1457 }
1458
1459 if let Some(path) = path {
1461 if index.is_some() {
1462 return Err(serde::de::Error::custom(
1463 "cannot specify both `path` and `index`",
1464 ));
1465 }
1466 if workspace.is_some() {
1467 return Err(serde::de::Error::custom(
1468 "cannot specify both `path` and `workspace`",
1469 ));
1470 }
1471 if git.is_some() {
1472 return Err(serde::de::Error::custom(
1473 "cannot specify both `path` and `git`",
1474 ));
1475 }
1476 if url.is_some() {
1477 return Err(serde::de::Error::custom(
1478 "cannot specify both `path` and `url`",
1479 ));
1480 }
1481 if rev.is_some() {
1482 return Err(serde::de::Error::custom(
1483 "cannot specify both `path` and `rev`",
1484 ));
1485 }
1486 if tag.is_some() {
1487 return Err(serde::de::Error::custom(
1488 "cannot specify both `path` and `tag`",
1489 ));
1490 }
1491 if branch.is_some() {
1492 return Err(serde::de::Error::custom(
1493 "cannot specify both `path` and `branch`",
1494 ));
1495 }
1496
1497 if editable == Some(true) && package == Some(false) {
1499 return Err(serde::de::Error::custom(
1500 "cannot specify both `editable = true` and `package = false`",
1501 ));
1502 }
1503
1504 return Ok(Self::Path {
1505 path,
1506 editable,
1507 package,
1508 marker,
1509 extra,
1510 group,
1511 });
1512 }
1513
1514 if let Some(index) = index {
1516 if workspace.is_some() {
1517 return Err(serde::de::Error::custom(
1518 "cannot specify both `index` and `workspace`",
1519 ));
1520 }
1521 if git.is_some() {
1522 return Err(serde::de::Error::custom(
1523 "cannot specify both `index` and `git`",
1524 ));
1525 }
1526 if url.is_some() {
1527 return Err(serde::de::Error::custom(
1528 "cannot specify both `index` and `url`",
1529 ));
1530 }
1531 if path.is_some() {
1532 return Err(serde::de::Error::custom(
1533 "cannot specify both `index` and `path`",
1534 ));
1535 }
1536 if rev.is_some() {
1537 return Err(serde::de::Error::custom(
1538 "cannot specify both `index` and `rev`",
1539 ));
1540 }
1541 if tag.is_some() {
1542 return Err(serde::de::Error::custom(
1543 "cannot specify both `index` and `tag`",
1544 ));
1545 }
1546 if branch.is_some() {
1547 return Err(serde::de::Error::custom(
1548 "cannot specify both `index` and `branch`",
1549 ));
1550 }
1551 if editable.is_some() {
1552 return Err(serde::de::Error::custom(
1553 "cannot specify both `index` and `editable`",
1554 ));
1555 }
1556 if package.is_some() {
1557 return Err(serde::de::Error::custom(
1558 "cannot specify both `index` and `package`",
1559 ));
1560 }
1561
1562 return Ok(Self::Registry {
1563 index,
1564 marker,
1565 extra,
1566 group,
1567 });
1568 }
1569
1570 if let Some(workspace) = workspace {
1572 if index.is_some() {
1573 return Err(serde::de::Error::custom(
1574 "cannot specify both `workspace` and `index`",
1575 ));
1576 }
1577 if git.is_some() {
1578 return Err(serde::de::Error::custom(
1579 "cannot specify both `workspace` and `git`",
1580 ));
1581 }
1582 if url.is_some() {
1583 return Err(serde::de::Error::custom(
1584 "cannot specify both `workspace` and `url`",
1585 ));
1586 }
1587 if path.is_some() {
1588 return Err(serde::de::Error::custom(
1589 "cannot specify both `workspace` and `path`",
1590 ));
1591 }
1592 if rev.is_some() {
1593 return Err(serde::de::Error::custom(
1594 "cannot specify both `workspace` and `rev`",
1595 ));
1596 }
1597 if tag.is_some() {
1598 return Err(serde::de::Error::custom(
1599 "cannot specify both `workspace` and `tag`",
1600 ));
1601 }
1602 if branch.is_some() {
1603 return Err(serde::de::Error::custom(
1604 "cannot specify both `workspace` and `branch`",
1605 ));
1606 }
1607 if package.is_some() {
1608 return Err(serde::de::Error::custom(
1609 "cannot specify both `workspace` and `package`",
1610 ));
1611 }
1612
1613 return Ok(Self::Workspace {
1614 workspace,
1615 editable,
1616 marker,
1617 extra,
1618 group,
1619 });
1620 }
1621
1622 Err(serde::de::Error::custom(
1624 "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1625 ))
1626 }
1627}
1628
1629#[derive(Error, Debug)]
1630pub enum SourceError {
1631 #[error("Failed to resolve Git reference: `{0}`")]
1632 UnresolvedReference(String),
1633 #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1634 WorkspacePackageGit(String),
1635 #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1636 WorkspacePackageUrl(String),
1637 #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1638 WorkspacePackageFile(String),
1639 #[error(
1640 "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1641 )]
1642 UnusedRev(String, String),
1643 #[error(
1644 "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1645 )]
1646 UnusedTag(String, String),
1647 #[error(
1648 "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1649 )]
1650 UnusedBranch(String, String),
1651 #[error(
1652 "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1653 )]
1654 UnusedLfs(String),
1655 #[error(
1656 "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1657 )]
1658 UnusedEditable(String),
1659 #[error("Failed to resolve absolute path")]
1660 Absolute(#[from] std::io::Error),
1661 #[error("Path contains invalid characters: `{}`", _0.display())]
1662 NonUtf8Path(PathBuf),
1663 #[error("Source markers must be disjoint, but the following markers overlap: `{0}` and `{1}`.")]
1664 OverlappingMarkers(String, String, String),
1665 #[error(
1666 "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1667 )]
1668 MissingMarkers,
1669 #[error("Must provide at least one source")]
1670 EmptySources,
1671}
1672
1673impl uv_errors::Hint for SourceError {
1674 fn hints(&self) -> uv_errors::Hints<'_> {
1675 match self {
1676 Self::OverlappingMarkers(_, rhs, replacement) => {
1677 uv_errors::Hints::from(format!("replace `{rhs}` with `{replacement}`"))
1678 }
1679 _ => uv_errors::Hints::none(),
1680 }
1681 }
1682}
1683
1684impl Source {
1685 pub fn from_requirement(
1686 name: &PackageName,
1687 source: RequirementSource,
1688 workspace: bool,
1689 editable: Option<bool>,
1690 index: Option<IndexName>,
1691 rev: Option<String>,
1692 tag: Option<String>,
1693 branch: Option<String>,
1694 lfs: GitLfsSetting,
1695 root: &Path,
1696 existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1697 ) -> Result<Option<Self>, SourceError> {
1698 if !matches!(
1700 source,
1701 RequirementSource::GitDirectory { .. } | RequirementSource::GitPath { .. }
1702 ) && (branch.is_some()
1703 || tag.is_some()
1704 || rev.is_some()
1705 || matches!(lfs, GitLfsSetting::Enabled { .. }))
1706 {
1707 if let Some(sources) = existing_sources {
1708 if let Some(package_sources) = sources.get(name) {
1709 for existing_source in package_sources.iter() {
1710 if let Self::Git {
1711 git,
1712 subdirectory,
1713 path,
1714 marker,
1715 extra,
1716 group,
1717 ..
1718 } = existing_source
1719 {
1720 return Ok(Some(Self::Git {
1721 git: git.clone(),
1722 subdirectory: subdirectory.clone(),
1723 rev,
1724 tag,
1725 branch,
1726 lfs: lfs.into(),
1727 marker: *marker,
1728 path: path.clone(),
1729 extra: extra.clone(),
1730 group: group.clone(),
1731 }));
1732 }
1733 }
1734 }
1735 }
1736 if let Some(rev) = rev {
1737 return Err(SourceError::UnusedRev(name.to_string(), rev));
1738 }
1739 if let Some(tag) = tag {
1740 return Err(SourceError::UnusedTag(name.to_string(), tag));
1741 }
1742 if let Some(branch) = branch {
1743 return Err(SourceError::UnusedBranch(name.to_string(), branch));
1744 }
1745 if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1746 return Err(SourceError::UnusedLfs(name.to_string()));
1747 }
1748 }
1749
1750 if !workspace {
1752 if !matches!(source, RequirementSource::Directory { .. }) {
1753 if editable == Some(true) {
1754 return Err(SourceError::UnusedEditable(name.to_string()));
1755 }
1756 }
1757 }
1758
1759 if workspace {
1761 return match source {
1762 RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1763 Ok(Some(Self::Workspace {
1764 workspace: true,
1765 editable,
1766 marker: MarkerTree::TRUE,
1767 extra: None,
1768 group: None,
1769 }))
1770 }
1771 RequirementSource::Url { .. } => {
1772 Err(SourceError::WorkspacePackageUrl(name.to_string()))
1773 }
1774 RequirementSource::GitDirectory { .. } => {
1775 Err(SourceError::WorkspacePackageGit(name.to_string()))
1776 }
1777 RequirementSource::GitPath { .. } => {
1778 Err(SourceError::WorkspacePackageGit(name.to_string()))
1779 }
1780 RequirementSource::Path { .. } => {
1781 Err(SourceError::WorkspacePackageFile(name.to_string()))
1782 }
1783 };
1784 }
1785
1786 let source = match source {
1787 RequirementSource::Registry { index: Some(_), .. } => {
1788 return Ok(None);
1789 }
1790 RequirementSource::Registry { index: None, .. } => {
1791 if let Some(index) = index {
1792 Self::Registry {
1793 index,
1794 marker: MarkerTree::TRUE,
1795 extra: None,
1796 group: None,
1797 }
1798 } else {
1799 return Ok(None);
1800 }
1801 }
1802 RequirementSource::Path { install_path, .. } => Self::Path {
1803 editable: None,
1804 package: None,
1805 path: PortablePathBuf::from(
1806 relative_to(&install_path, root)
1807 .or_else(|_| std::path::absolute(&install_path))
1808 .map_err(SourceError::Absolute)?
1809 .into_boxed_path(),
1810 ),
1811 marker: MarkerTree::TRUE,
1812 extra: None,
1813 group: None,
1814 },
1815 RequirementSource::Directory {
1816 install_path,
1817 editable: is_editable,
1818 ..
1819 } => Self::Path {
1820 editable: editable.or(is_editable),
1821 package: None,
1822 path: PortablePathBuf::from(
1823 relative_to(&install_path, root)
1824 .or_else(|_| std::path::absolute(&install_path))
1825 .map_err(SourceError::Absolute)?
1826 .into_boxed_path(),
1827 ),
1828 marker: MarkerTree::TRUE,
1829 extra: None,
1830 group: None,
1831 },
1832 RequirementSource::Url {
1833 location,
1834 subdirectory,
1835 ..
1836 } => Self::Url {
1837 url: location,
1838 subdirectory: subdirectory.map(PortablePathBuf::from),
1839 marker: MarkerTree::TRUE,
1840 extra: None,
1841 group: None,
1842 },
1843 RequirementSource::GitDirectory {
1844 git, subdirectory, ..
1845 } => {
1846 if rev.is_none() && tag.is_none() && branch.is_none() {
1847 let rev = match git.reference() {
1848 GitReference::Branch(rev) => Some(rev),
1849 GitReference::Tag(rev) => Some(rev),
1850 GitReference::BranchOrTag(rev) => Some(rev),
1851 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1852 GitReference::NamedRef(rev) => Some(rev),
1853 GitReference::DefaultBranch => None,
1854 };
1855 Self::Git {
1856 rev: rev.cloned(),
1857 tag,
1858 branch,
1859 lfs: lfs.into(),
1860 git: git.url().clone(),
1861 subdirectory: subdirectory.map(PortablePathBuf::from),
1862 path: None,
1863 marker: MarkerTree::TRUE,
1864 extra: None,
1865 group: None,
1866 }
1867 } else {
1868 Self::Git {
1869 rev,
1870 tag,
1871 branch,
1872 lfs: lfs.into(),
1873 git: git.url().clone(),
1874 subdirectory: subdirectory.map(PortablePathBuf::from),
1875 path: None,
1876 marker: MarkerTree::TRUE,
1877 extra: None,
1878 group: None,
1879 }
1880 }
1881 }
1882 RequirementSource::GitPath {
1883 git, install_path, ..
1884 } => {
1885 if rev.is_none() && tag.is_none() && branch.is_none() {
1886 let rev = match git.reference() {
1887 GitReference::Branch(rev) => Some(rev),
1888 GitReference::Tag(rev) => Some(rev),
1889 GitReference::BranchOrTag(rev) => Some(rev),
1890 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1891 GitReference::NamedRef(rev) => Some(rev),
1892 GitReference::DefaultBranch => None,
1893 };
1894 Self::Git {
1895 rev: rev.cloned(),
1896 tag,
1897 branch,
1898 lfs: lfs.into(),
1899 git: git.url().clone(),
1900 subdirectory: None,
1901 path: Some(PortablePathBuf::from(install_path.as_path())),
1902 marker: MarkerTree::TRUE,
1903 extra: None,
1904 group: None,
1905 }
1906 } else {
1907 Self::Git {
1908 rev,
1909 tag,
1910 branch,
1911 lfs: lfs.into(),
1912 git: git.url().clone(),
1913 subdirectory: None,
1914 path: Some(PortablePathBuf::from(install_path.as_path())),
1915 marker: MarkerTree::TRUE,
1916 extra: None,
1917 group: None,
1918 }
1919 }
1920 }
1921 };
1922
1923 Ok(Some(source))
1924 }
1925
1926 pub fn marker(&self) -> MarkerTree {
1928 match self {
1929 Self::Git { marker, .. } => *marker,
1930 Self::Url { marker, .. } => *marker,
1931 Self::Path { marker, .. } => *marker,
1932 Self::Registry { marker, .. } => *marker,
1933 Self::Workspace { marker, .. } => *marker,
1934 }
1935 }
1936
1937 pub fn extra(&self) -> Option<&ExtraName> {
1939 match self {
1940 Self::Git { extra, .. } => extra.as_ref(),
1941 Self::Url { extra, .. } => extra.as_ref(),
1942 Self::Path { extra, .. } => extra.as_ref(),
1943 Self::Registry { extra, .. } => extra.as_ref(),
1944 Self::Workspace { extra, .. } => extra.as_ref(),
1945 }
1946 }
1947
1948 pub fn group(&self) -> Option<&GroupName> {
1950 match self {
1951 Self::Git { group, .. } => group.as_ref(),
1952 Self::Url { group, .. } => group.as_ref(),
1953 Self::Path { group, .. } => group.as_ref(),
1954 Self::Registry { group, .. } => group.as_ref(),
1955 Self::Workspace { group, .. } => group.as_ref(),
1956 }
1957 }
1958}
1959
1960#[derive(Debug, Clone, PartialEq, Eq)]
1962pub enum DependencyType {
1963 Production,
1965 Dev,
1967 Optional(ExtraName),
1969 Group(GroupName),
1971}
1972
1973impl DependencyType {
1974 pub fn toml_table_name(&self) -> String {
1976 match self {
1977 Self::Production => "`project.dependencies`".to_string(),
1978 Self::Dev => {
1979 "`tool.uv.dev-dependencies` or `tool.uv.dependency-groups.dev`".to_string()
1980 }
1981 Self::Optional(extra) => format!("`project.optional-dependencies.{extra}`"),
1982 Self::Group(group) => format!("`dependency-groups.{group}`"),
1983 }
1984 }
1985}
1986
1987#[derive(Debug, Clone, PartialEq, Eq)]
1988#[cfg_attr(test, derive(Serialize))]
1989pub(crate) struct BuildBackendSettingsSchema;
1990
1991impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1992 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1993 where
1994 D: Deserializer<'de>,
1995 {
1996 Ok(Self)
1997 }
1998}
1999
2000#[cfg(feature = "schemars")]
2001impl schemars::JsonSchema for BuildBackendSettingsSchema {
2002 fn schema_name() -> Cow<'static, str> {
2003 BuildBackendSettings::schema_name()
2004 }
2005
2006 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
2007 BuildBackendSettings::json_schema(generator)
2008 }
2009}
2010
2011impl OptionsMetadata for BuildBackendSettingsSchema {
2012 fn record(visit: &mut dyn Visit) {
2013 BuildBackendSettings::record(visit);
2014 }
2015
2016 fn documentation() -> Option<&'static str> {
2017 BuildBackendSettings::documentation()
2018 }
2019
2020 fn metadata() -> OptionSet
2021 where
2022 Self: Sized + 'static,
2023 {
2024 BuildBackendSettings::metadata()
2025 }
2026}