Skip to main content

uv_workspace/
pyproject.rs

1//! Reads the following fields from `pyproject.toml`:
2//!
3//! * `project.{dependencies,optional-dependencies}`
4//! * `tool.uv.sources`
5//! * `tool.uv.workspace`
6//!
7//! Then lowers them into a dependency specification.
8
9#[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
55/// Helper function to deserialize a map while ensuring all keys are unique.
56fn 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/// A `pyproject.toml` as specified in PEP 517.
105#[derive(Deserialize, Debug, Clone)]
106#[cfg_attr(test, derive(Serialize))]
107#[serde(rename_all = "kebab-case")]
108pub struct PyProjectToml {
109    /// PEP 621-compliant project metadata.
110    pub project: Option<Project>,
111    /// Tool-specific metadata.
112    pub tool: Option<Tool>,
113    /// Non-project dependency groups, as defined in PEP 735.
114    pub dependency_groups: Option<DependencyGroups>,
115    /// The raw unserialized document.
116    #[serde(skip)]
117    pub raw: String,
118
119    /// Used to determine whether a `build-system` section is present.
120    #[serde(default, skip_serializing)]
121    pub build_system: Option<serde::de::IgnoredAny>,
122}
123
124impl PyProjectToml {
125    /// Parse a `PyProjectToml` from a raw TOML string.
126    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    /// Returns `true` if the project should be considered a Python package, as opposed to a
135    /// non-package ("virtual") project.
136    pub fn is_package(&self, require_build_system: bool) -> bool {
137        // If `tool.uv.package` is set, defer to that explicit setting.
138        if let Some(is_package) = self.tool_uv_package() {
139            return is_package;
140        }
141
142        // Otherwise, a project is assumed to be a package if `build-system` is present.
143        self.build_system.is_some() || !require_build_system
144    }
145
146    /// Returns the value of `tool.uv.package` if set.
147    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    /// Returns `true` if the project uses a dynamic version.
155    pub fn is_dynamic(&self) -> bool {
156        self.project
157            .as_ref()
158            .is_some_and(|project| project.version.is_none())
159    }
160
161    /// Returns whether the project manifest contains any script table.
162    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    /// Returns the set of conflicts for the project.
171    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
189// Ignore raw document in comparison.
190impl 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/// PEP 621 project metadata (`project`).
205///
206/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
207#[derive(Deserialize, Debug, Clone, PartialEq)]
208#[cfg_attr(test, derive(Serialize))]
209#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
210pub struct Project {
211    /// The name of the project
212    pub name: PackageName,
213    /// The version of the project
214    pub version: Option<Version>,
215    /// The Python versions this project is compatible with.
216    pub requires_python: Option<VersionSpecifiers>,
217    /// The dependencies of the project.
218    pub dependencies: Option<Vec<String>>,
219    /// The optional dependencies of the project.
220    pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
221
222    /// Used to determine whether a `gui-scripts` section is present.
223    #[serde(default, skip_serializing)]
224    pub(crate) gui_scripts: Option<serde::de::IgnoredAny>,
225    /// Used to determine whether a `scripts` section is present.
226    #[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        // If `[project.name]` is not present, show a dedicated error message.
248        let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
249
250        // If `[project.version]` is not present (or listed in `[project.dynamic]`), show a dedicated error message.
251        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
279/// Validates that index names in the `tool.uv.index` field are unique.
280///
281/// This custom deserializer function checks for duplicate index names
282/// and returns an error if any duplicates are found.
283fn 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// NOTE(charlie): When adding fields to this struct, mark them as ignored on `Options` in
304// `crates/uv-settings/src/settings.rs`.
305#[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    /// The sources to use when resolving dependencies.
311    ///
312    /// `tool.uv.sources` enriches the dependency metadata with additional sources, incorporated
313    /// during development. A dependency source can be a Git repository, a URL, a local path, or an
314    /// alternative registry.
315    ///
316    /// See [Dependencies](../concepts/projects/dependencies.md) for more.
317    #[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    /// The indexes to use when resolving dependencies.
330    ///
331    /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)
332    /// (the simple repository API), or a local directory laid out in the same format.
333    ///
334    /// Indexes are considered in the order in which they're defined, such that the first-defined
335    /// index has the highest priority. Further, the indexes provided by this setting are given
336    /// higher priority than any indexes specified via [`index_url`](#index-url) or
337    /// [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains
338    /// a given package, unless an alternative [index strategy](#index-strategy) is specified.
339    ///
340    /// If an index is marked as `explicit = true`, it will be used exclusively for the
341    /// dependencies that select it explicitly via `[tool.uv.sources]`, as in:
342    ///
343    /// ```toml
344    /// [[tool.uv.index]]
345    /// name = "pytorch"
346    /// url = "https://download.pytorch.org/whl/cu121"
347    /// explicit = true
348    ///
349    /// [tool.uv.sources]
350    /// torch = { index = "pytorch" }
351    /// ```
352    ///
353    /// If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is
354    /// given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
355    /// PyPI default index.
356    #[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    /// The workspace definition for the project, if any.
369    #[option_group]
370    pub workspace: Option<ToolUvWorkspace>,
371
372    /// Whether the project is managed by uv. If `false`, uv will ignore the project when
373    /// `uv run` is invoked.
374    #[option(
375        default = r#"true"#,
376        value_type = "bool",
377        example = r#"
378            managed = false
379        "#
380    )]
381    pub managed: Option<bool>,
382
383    /// Whether the project should be considered a Python package, or a non-package ("virtual")
384    /// project.
385    ///
386    /// Packages are built and installed into the virtual environment in editable mode and thus
387    /// require a build backend, while virtual projects are _not_ built or installed; instead, only
388    /// their dependencies are included in the virtual environment.
389    ///
390    /// Creating a package requires that a `build-system` is present in the `pyproject.toml`, and
391    /// that the project adheres to a structure that adheres to the build backend's expectations
392    /// (e.g., a `src` layout).
393    #[option(
394        default = r#"true"#,
395        value_type = "bool",
396        example = r#"
397            package = false
398        "#
399    )]
400    pub package: Option<bool>,
401
402    /// The list of `dependency-groups` to install by default.
403    ///
404    /// Can also be the literal `"all"` to default enable all groups.
405    #[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    /// Additional settings for `dependency-groups`.
415    ///
416    /// Currently this can only be used to add `requires-python` constraints
417    /// to dependency groups (typically to inform uv that your dev tooling
418    /// has a higher python requirement than your actual project).
419    ///
420    /// This cannot be used to define dependency groups, use the top-level
421    /// `[dependency-groups]` table for that.
422    #[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    /// The project's development dependencies.
433    ///
434    /// Development dependencies will be installed by default in `uv run` and `uv sync`, but will
435    /// not appear in the project's published metadata.
436    ///
437    /// Use of this field is not recommend anymore. Instead, use the `dependency-groups.dev` field
438    /// which is a standardized way to declare development dependencies. The contents of
439    /// `tool.uv.dev-dependencies` and `dependency-groups.dev` are combined to determine the final
440    /// requirements of the `dev` dependency group.
441    #[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    /// Overrides to apply when resolving the project's dependencies.
458    ///
459    /// Overrides are used to force selection of a specific version of a package, regardless of the
460    /// version requested by any other package, and regardless of whether choosing that version
461    /// would typically constitute an invalid resolution.
462    ///
463    /// While constraints are _additive_, in that they're combined with the requirements of the
464    /// constituent packages, overrides are _absolute_, in that they completely replace the
465    /// requirements of any constituent packages.
466    ///
467    /// Including a package as an override will _not_ trigger installation of the package on its
468    /// own; instead, the package must be requested elsewhere in the project's first-party or
469    /// transitive dependencies.
470    ///
471    /// !!! note
472    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `override-dependencies` from
473    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
474    ///     workspace members or `uv.toml` files.
475    #[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    /// Dependencies to exclude when resolving the project's dependencies.
494    ///
495    /// Excludes are used to prevent a package from being selected during resolution,
496    /// regardless of whether it's requested by any other package. When a package is excluded,
497    /// it will be omitted from the dependency list entirely.
498    ///
499    /// Including a package as an exclusion will prevent it from being installed, even if
500    /// it's requested by transitive dependencies. This can be useful for removing optional
501    /// dependencies or working around packages with broken dependencies.
502    ///
503    /// !!! note
504    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `exclude-dependencies` from
505    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
506    ///     workspace members or `uv.toml` files.
507    #[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    /// Constraints to apply when resolving the project's dependencies.
525    ///
526    /// Constraints are used to restrict the versions of dependencies that are selected during
527    /// resolution.
528    ///
529    /// Including a package as a constraint will _not_ trigger installation of the package on its
530    /// own; instead, the package must be requested elsewhere in the project's first-party or
531    /// transitive dependencies.
532    ///
533    /// !!! note
534    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `constraint-dependencies` from
535    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
536    ///     workspace members or `uv.toml` files.
537    #[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    /// Constraints to apply when solving build dependencies.
556    ///
557    /// Build constraints are used to restrict the versions of build dependencies that are selected
558    /// when building a package during resolution or installation.
559    ///
560    /// Including a package as a constraint will _not_ trigger installation of the package during
561    /// a build; instead, the package must be requested elsewhere in the project's build dependency
562    /// graph.
563    ///
564    /// !!! note
565    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `build-constraint-dependencies` from
566    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
567    ///     workspace members or `uv.toml` files.
568    #[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    /// A list of supported environments against which to resolve dependencies.
587    ///
588    /// By default, uv will resolve for all possible environments during a `uv lock` operation.
589    /// However, you can restrict the set of supported environments to improve performance and avoid
590    /// unsatisfiable branches in the solution space.
591    ///
592    /// These environments will also be respected when `uv pip compile` is invoked with the
593    /// `--universal` flag.
594    #[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    /// A list of required platforms, for packages that lack source distributions.
612    ///
613    /// When a package does not have a source distribution, it's availability will be limited to
614    /// the platforms supported by its built distributions (wheels). For example, if a package only
615    /// publishes wheels for Linux, then it won't be installable on macOS or Windows.
616    ///
617    /// By default, uv requires each package to include at least one wheel that is compatible with
618    /// the designated Python version. The `required-environments` setting can be used to ensure that
619    /// the resulting resolution contains wheels for specific platforms, or fails if no such wheels
620    /// are available.
621    ///
622    /// While the `environments` setting _limits_ the set of environments that uv will consider when
623    /// resolving dependencies, `required-environments` _expands_ the set of platforms that uv _must_
624    /// support when resolving dependencies.
625    ///
626    /// For example, `environments = ["sys_platform == 'darwin'"]` would limit uv to solving for
627    /// macOS (and ignoring Linux and Windows). On the other hand, `required-environments = ["sys_platform == 'darwin'"]`
628    /// would _require_ that any package without a source distribution include a wheel for macOS in
629    /// order to be installable.
630    #[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    /// Declare collections of extras or dependency groups that are conflicting
651    /// (i.e., mutually exclusive).
652    ///
653    /// It's useful to declare conflicts when two or more extras have mutually
654    /// incompatible dependencies. For example, extra `foo` might depend
655    /// on `numpy==2.0.0` while extra `bar` depends on `numpy==2.1.0`. While these
656    /// dependencies conflict, it may be the case that users are not expected to
657    /// activate both `foo` and `bar` at the same time, making it possible to
658    /// generate a universal resolution for the project despite the incompatibility.
659    ///
660    /// By making such conflicts explicit, uv can generate a universal resolution
661    /// for a project, taking into account that certain combinations of extras and
662    /// groups are mutually exclusive. In exchange, installation will fail if a
663    /// user attempts to activate both conflicting extras.
664    #[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    // Only exists on this type for schema and docs generation, the build backend settings are
695    // never merged in a workspace and read separately by the backend code.
696    /// Configuration for the uv build backend.
697    ///
698    /// Note that those settings only apply when using the `uv_build` backend, other build backends
699    /// (such as hatchling) have their own configuration.
700    #[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    /// Returns the underlying `BTreeMap` of package names to sources.
711    pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
712        &self.0
713    }
714
715    /// Convert the [`ToolUvSources`] into its inner `BTreeMap`.
716    #[must_use]
717    pub fn into_inner(self) -> BTreeMap<PackageName, Sources> {
718        self.0
719    }
720}
721
722/// Ensure that all keys in the TOML table are unique.
723impl<'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    /// Returns the underlying `BTreeMap` of group names to settings.
742    pub fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
743        &self.0
744    }
745
746    /// Convert the [`ToolUvDependencyGroups`] into its inner `BTreeMap`.
747    #[must_use]
748    pub fn into_inner(self) -> BTreeMap<GroupName, DependencyGroupSettings> {
749        self.0
750    }
751}
752
753/// Ensure that all keys in the TOML table are unique.
754impl<'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    /// Version of python to require when installing this group
772    #[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
868/// Ensure that all keys in the TOML table are unique.
869impl<'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    /// Packages to include as workspace members.
887    ///
888    /// Supports both globs and explicit paths.
889    ///
890    /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
891    #[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    /// Packages to exclude as workspace members. If a package matches both `members` and
900    /// `exclude`, it will be excluded.
901    ///
902    /// Supports both globs and explicit paths.
903    ///
904    /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
905    #[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/// (De)serialize globs as strings.
916#[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    /// Return an [`Iterator`] over the sources.
976    ///
977    /// If the iterator contains multiple entries, they will always use disjoint markers.
978    ///
979    /// The iterator will contain at most one registry source.
980    pub fn iter(&self) -> impl Iterator<Item = &Source> {
981        self.0.iter()
982    }
983
984    /// Returns `true` if the sources list is empty.
985    pub fn is_empty(&self) -> bool {
986        self.0.is_empty()
987    }
988
989    /// Returns the number of sources in the list.
990    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                // Ensure that there is at least one source.
1095                if sources.is_empty() {
1096                    return Err(SourceError::EmptySources);
1097                }
1098
1099                Ok(Self(sources))
1100            }
1101        }
1102    }
1103}
1104
1105/// A `tool.uv.sources` value.
1106#[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    /// A remote Git repository, available over HTTPS or SSH.
1111    ///
1112    /// Example:
1113    /// ```toml
1114    /// flask = { git = "https://github.com/pallets/flask", tag = "3.0.0" }
1115    /// ```
1116    Git {
1117        /// The repository URL (without the `git+` prefix).
1118        git: DisplaySafeUrl,
1119        /// The path to the directory with the `pyproject.toml`, if it's not in the archive root.
1120        subdirectory: Option<PortablePathBuf>,
1121        // Only one of the three may be used; we'll validate this later and emit a custom error.
1122        rev: Option<String>,
1123        tag: Option<String>,
1124        branch: Option<String>,
1125        /// Whether to use Git LFS when cloning the repository.
1126        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    /// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution
1137    /// (`.zip`, `.tar.gz`).
1138    ///
1139    /// Example:
1140    /// ```toml
1141    /// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" }
1142    /// ```
1143    Url {
1144        url: DisplaySafeUrl,
1145        /// For source distributions, the path to the directory with the `pyproject.toml`, if it's
1146        /// not in the archive root.
1147        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    /// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
1158    /// `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
1159    /// `setup.py` file in the root).
1160    Path {
1161        path: PortablePathBuf,
1162        /// `false` by default.
1163        editable: Option<bool>,
1164        /// Whether to treat the dependency as a buildable Python package (`true`) or as a virtual
1165        /// package (`false`). If `false`, the package will not be built or installed, but its
1166        /// dependencies will be included in the virtual environment.
1167        ///
1168        /// When omitted, the package status is inferred based on the presence of a `[build-system]`
1169        /// in the project's `pyproject.toml`.
1170        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    /// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
1181    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    /// A dependency on another package in the workspace.
1193    Workspace {
1194        /// When set to `false`, the package will be fetched from the remote index, rather than
1195        /// included as a workspace package.
1196        workspace: bool,
1197        /// Whether the package should be installed as editable. Defaults to `true`.
1198        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
1210/// A custom deserialization implementation for [`Source`]. This is roughly equivalent to
1211/// `#[serde(untagged)]`, but provides more detailed error messages.
1212impl<'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        // Attempt to deserialize as `CatchAll`.
1243        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 both `extra` and `group` are set, return an error.
1262        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 the `git` field is set, we're dealing with a Git source.
1269        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            // At most one of `rev`, `tag`, or `branch` may be set.
1302            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            // If the user prefixed the URL with `git+`, strip it.
1315            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 the `url` field is set, we're dealing with a URL source.
1335        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 the `path` field is set, we're dealing with a path source.
1392        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            // A project must be packaged in order to be installed as editable.
1430            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 the `index` field is set, we're dealing with a registry source.
1447        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 the `workspace` field is set, we're dealing with a workspace source.
1503        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        // If none of the fields are set, we're dealing with an error.
1555        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 the user specified a Git reference for a non-Git source, try existing Git sources before erroring.
1621        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 we resolved a non-path source, and user specified an `--editable` flag, error.
1669        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 the source is a workspace package, error if the user tried to specify a source.
1678        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    /// Return the [`MarkerTree`] for the source.
1801    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    /// Return the extra name for the source.
1812    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    /// Return the dependency group name for the source.
1823    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/// The type of a dependency in a `pyproject.toml`.
1835#[derive(Debug, Clone, PartialEq, Eq)]
1836pub enum DependencyType {
1837    /// A dependency in `project.dependencies`.
1838    Production,
1839    /// A dependency in `tool.uv.dev-dependencies`.
1840    Dev,
1841    /// A dependency in `project.optional-dependencies.{0}`.
1842    Optional(ExtraName),
1843    /// A dependency in `dependency-groups.{0}`.
1844    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}