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_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    TomlSyntax(#[from] toml_edit::TomlError),
42    #[error(transparent)]
43    TomlSchema(#[from] toml_edit::de::Error),
44    #[error(
45        "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
46    )]
47    MissingName,
48    #[error(
49        "`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list"
50    )]
51    MissingVersion,
52}
53
54/// Helper function to deserialize a map while ensuring all keys are unique.
55fn deserialize_unique_map<'de, D, K, V, F>(
56    deserializer: D,
57    error_msg: F,
58) -> Result<BTreeMap<K, V>, D::Error>
59where
60    D: Deserializer<'de>,
61    K: Deserialize<'de> + Ord + std::fmt::Display,
62    V: Deserialize<'de>,
63    F: FnOnce(&K) -> String,
64{
65    struct Visitor<K, V, F>(F, std::marker::PhantomData<(K, V)>);
66
67    impl<'de, K, V, F> serde::de::Visitor<'de> for Visitor<K, V, F>
68    where
69        K: Deserialize<'de> + Ord + std::fmt::Display,
70        V: Deserialize<'de>,
71        F: FnOnce(&K) -> String,
72    {
73        type Value = BTreeMap<K, V>;
74
75        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
76            formatter.write_str("a map with unique keys")
77        }
78
79        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
80        where
81            M: serde::de::MapAccess<'de>,
82        {
83            use std::collections::btree_map::Entry;
84
85            let mut map = BTreeMap::new();
86            while let Some((key, value)) = access.next_entry::<K, V>()? {
87                match map.entry(key) {
88                    Entry::Occupied(entry) => {
89                        return Err(serde::de::Error::custom((self.0)(entry.key())));
90                    }
91                    Entry::Vacant(entry) => {
92                        entry.insert(value);
93                    }
94                }
95            }
96            Ok(map)
97        }
98    }
99
100    deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData))
101}
102
103/// A `pyproject.toml` as specified in PEP 517.
104#[derive(Deserialize, Debug, Clone)]
105#[cfg_attr(test, derive(Serialize))]
106#[serde(rename_all = "kebab-case")]
107pub struct PyProjectToml {
108    /// PEP 621-compliant project metadata.
109    pub project: Option<Project>,
110    /// Tool-specific metadata.
111    pub tool: Option<Tool>,
112    /// Non-project dependency groups, as defined in PEP 735.
113    pub dependency_groups: Option<DependencyGroups>,
114    /// The raw unserialized document.
115    #[serde(skip)]
116    pub raw: String,
117
118    /// Used to determine whether a `build-system` section is present.
119    #[serde(default, skip_serializing)]
120    pub build_system: Option<serde::de::IgnoredAny>,
121}
122
123impl PyProjectToml {
124    /// Parse a `PyProjectToml` from a raw TOML string.
125    pub fn from_string(raw: String) -> Result<Self, PyprojectTomlError> {
126        let pyproject =
127            toml_edit::Document::from_str(&raw).map_err(PyprojectTomlError::TomlSyntax)?;
128        let pyproject = Self::deserialize(pyproject.into_deserializer())
129            .map_err(PyprojectTomlError::TomlSchema)?;
130        Ok(Self { raw, ..pyproject })
131    }
132
133    /// Returns `true` if the project should be considered a Python package, as opposed to a
134    /// non-package ("virtual") project.
135    pub fn is_package(&self, require_build_system: bool) -> bool {
136        // If `tool.uv.package` is set, defer to that explicit setting.
137        if let Some(is_package) = self.tool_uv_package() {
138            return is_package;
139        }
140
141        // Otherwise, a project is assumed to be a package if `build-system` is present.
142        self.build_system.is_some() || !require_build_system
143    }
144
145    /// Returns the value of `tool.uv.package` if set.
146    fn tool_uv_package(&self) -> Option<bool> {
147        self.tool
148            .as_ref()
149            .and_then(|tool| tool.uv.as_ref())
150            .and_then(|uv| uv.package)
151    }
152
153    /// Returns `true` if the project uses a dynamic version.
154    pub fn is_dynamic(&self) -> bool {
155        self.project
156            .as_ref()
157            .is_some_and(|project| project.version.is_none())
158    }
159
160    /// Returns whether the project manifest contains any script table.
161    pub fn has_scripts(&self) -> bool {
162        if let Some(ref project) = self.project {
163            project.gui_scripts.is_some() || project.scripts.is_some()
164        } else {
165            false
166        }
167    }
168
169    /// Returns the set of conflicts for the project.
170    pub fn conflicts(&self) -> Conflicts {
171        let empty = Conflicts::empty();
172        let Some(project) = self.project.as_ref() else {
173            return empty;
174        };
175        let Some(tool) = self.tool.as_ref() else {
176            return empty;
177        };
178        let Some(tooluv) = tool.uv.as_ref() else {
179            return empty;
180        };
181        let Some(conflicting) = tooluv.conflicts.as_ref() else {
182            return empty;
183        };
184        conflicting.to_conflicts_with_package_name(&project.name)
185    }
186}
187
188// Ignore raw document in comparison.
189impl PartialEq for PyProjectToml {
190    fn eq(&self, other: &Self) -> bool {
191        self.project.eq(&other.project) && self.tool.eq(&other.tool)
192    }
193}
194
195impl Eq for PyProjectToml {}
196
197impl AsRef<[u8]> for PyProjectToml {
198    fn as_ref(&self) -> &[u8] {
199        self.raw.as_bytes()
200    }
201}
202
203/// PEP 621 project metadata (`project`).
204///
205/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
206#[derive(Deserialize, Debug, Clone, PartialEq)]
207#[cfg_attr(test, derive(Serialize))]
208#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
209pub struct Project {
210    /// The name of the project
211    pub name: PackageName,
212    /// The version of the project
213    pub version: Option<Version>,
214    /// The Python versions this project is compatible with.
215    pub requires_python: Option<VersionSpecifiers>,
216    /// The dependencies of the project.
217    pub dependencies: Option<Vec<String>>,
218    /// The optional dependencies of the project.
219    pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
220
221    /// Used to determine whether a `gui-scripts` section is present.
222    #[serde(default, skip_serializing)]
223    pub(crate) gui_scripts: Option<serde::de::IgnoredAny>,
224    /// Used to determine whether a `scripts` section is present.
225    #[serde(default, skip_serializing)]
226    pub(crate) scripts: Option<serde::de::IgnoredAny>,
227}
228
229#[derive(Deserialize, Debug)]
230#[serde(rename_all = "kebab-case")]
231struct ProjectWire {
232    name: Option<PackageName>,
233    version: Option<Version>,
234    dynamic: Option<Vec<String>>,
235    requires_python: Option<VersionSpecifiers>,
236    dependencies: Option<Vec<String>>,
237    optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
238    gui_scripts: Option<serde::de::IgnoredAny>,
239    scripts: Option<serde::de::IgnoredAny>,
240}
241
242impl TryFrom<ProjectWire> for Project {
243    type Error = PyprojectTomlError;
244
245    fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
246        // If `[project.name]` is not present, show a dedicated error message.
247        let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
248
249        // If `[project.version]` is not present (or listed in `[project.dynamic]`), show a dedicated error message.
250        if value.version.is_none()
251            && !value
252                .dynamic
253                .as_ref()
254                .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
255        {
256            return Err(PyprojectTomlError::MissingVersion);
257        }
258
259        Ok(Self {
260            name,
261            version: value.version,
262            requires_python: value.requires_python,
263            dependencies: value.dependencies,
264            optional_dependencies: value.optional_dependencies,
265            gui_scripts: value.gui_scripts,
266            scripts: value.scripts,
267        })
268    }
269}
270
271#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
272#[cfg_attr(test, derive(Serialize))]
273#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
274pub struct Tool {
275    pub uv: Option<ToolUv>,
276}
277
278/// Validates that index names in the `tool.uv.index` field are unique.
279///
280/// This custom deserializer function checks for duplicate index names
281/// and returns an error if any duplicates are found.
282fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
283where
284    D: Deserializer<'de>,
285{
286    let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
287    if let Some(indexes) = indexes.as_ref() {
288        let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
289        for index in indexes {
290            if let Some(name) = index.name.as_ref() {
291                if !seen_names.insert(name) {
292                    return Err(serde::de::Error::custom(format!(
293                        "duplicate index name `{name}`"
294                    )));
295                }
296            }
297        }
298    }
299    Ok(indexes)
300}
301
302// NOTE(charlie): When adding fields to this struct, mark them as ignored on `Options` in
303// `crates/uv-settings/src/settings.rs`.
304#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
305#[cfg_attr(test, derive(Serialize))]
306#[serde(rename_all = "kebab-case")]
307#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
308pub struct ToolUv {
309    /// The sources to use when resolving dependencies.
310    ///
311    /// `tool.uv.sources` enriches the dependency metadata with additional sources, incorporated
312    /// during development. A dependency source can be a Git repository, a URL, a local path, or an
313    /// alternative registry.
314    ///
315    /// See [Dependencies](../concepts/projects/dependencies.md) for more.
316    #[option(
317        default = "{}",
318        value_type = "dict",
319        example = r#"
320            [tool.uv.sources]
321            httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
322            pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
323            pydantic = { path = "/path/to/pydantic", editable = true }
324        "#
325    )]
326    pub sources: Option<ToolUvSources>,
327
328    /// The indexes to use when resolving dependencies.
329    ///
330    /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)
331    /// (the simple repository API), or a local directory laid out in the same format.
332    ///
333    /// Indexes are considered in the order in which they're defined, such that the first-defined
334    /// index has the highest priority. Further, the indexes provided by this setting are given
335    /// higher priority than any indexes specified via [`index_url`](#index-url) or
336    /// [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains
337    /// a given package, unless an alternative [index strategy](#index-strategy) is specified.
338    ///
339    /// If an index is marked as `explicit = true`, it will be used exclusively for the
340    /// dependencies that select it explicitly via `[tool.uv.sources]`, as in:
341    ///
342    /// ```toml
343    /// [[tool.uv.index]]
344    /// name = "pytorch"
345    /// url = "https://download.pytorch.org/whl/cu121"
346    /// explicit = true
347    ///
348    /// [tool.uv.sources]
349    /// torch = { index = "pytorch" }
350    /// ```
351    ///
352    /// If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is
353    /// given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
354    /// PyPI default index.
355    #[option(
356        default = "[]",
357        value_type = "dict",
358        example = r#"
359            [[tool.uv.index]]
360            name = "pytorch"
361            url = "https://download.pytorch.org/whl/cu121"
362        "#
363    )]
364    #[serde(deserialize_with = "deserialize_index_vec", default)]
365    pub index: Option<Vec<Index>>,
366
367    /// The workspace definition for the project, if any.
368    #[option_group]
369    pub workspace: Option<ToolUvWorkspace>,
370
371    /// Whether the project is managed by uv. If `false`, uv will ignore the project when
372    /// `uv run` is invoked.
373    #[option(
374        default = r#"true"#,
375        value_type = "bool",
376        example = r#"
377            managed = false
378        "#
379    )]
380    pub managed: Option<bool>,
381
382    /// Whether the project should be considered a Python package, or a non-package ("virtual")
383    /// project.
384    ///
385    /// Packages are built and installed into the virtual environment in editable mode and thus
386    /// require a build backend, while virtual projects are _not_ built or installed; instead, only
387    /// their dependencies are included in the virtual environment.
388    ///
389    /// Creating a package requires that a `build-system` is present in the `pyproject.toml`, and
390    /// that the project adheres to a structure that adheres to the build backend's expectations
391    /// (e.g., a `src` layout).
392    #[option(
393        default = r#"true"#,
394        value_type = "bool",
395        example = r#"
396            package = false
397        "#
398    )]
399    pub package: Option<bool>,
400
401    /// The list of `dependency-groups` to install by default.
402    ///
403    /// Can also be the literal `"all"` to default enable all groups.
404    #[option(
405        default = r#"["dev"]"#,
406        value_type = r#"str | list[str]"#,
407        example = r#"
408            default-groups = ["docs"]
409        "#
410    )]
411    pub default_groups: Option<DefaultGroups>,
412
413    /// Additional settings for `dependency-groups`.
414    ///
415    /// Currently this can only be used to add `requires-python` constraints
416    /// to dependency groups (typically to inform uv that your dev tooling
417    /// has a higher python requirement than your actual project).
418    ///
419    /// This cannot be used to define dependency groups, use the top-level
420    /// `[dependency-groups]` table for that.
421    #[option(
422        default = "[]",
423        value_type = "dict",
424        example = r#"
425            [tool.uv.dependency-groups]
426            my-group = {requires-python = ">=3.12"}
427        "#
428    )]
429    pub dependency_groups: Option<ToolUvDependencyGroups>,
430
431    /// The project's development dependencies.
432    ///
433    /// Development dependencies will be installed by default in `uv run` and `uv sync`, but will
434    /// not appear in the project's published metadata.
435    ///
436    /// Use of this field is not recommend anymore. Instead, use the `dependency-groups.dev` field
437    /// which is a standardized way to declare development dependencies. The contents of
438    /// `tool.uv.dev-dependencies` and `dependency-groups.dev` are combined to determine the final
439    /// requirements of the `dev` dependency group.
440    #[cfg_attr(
441        feature = "schemars",
442        schemars(
443            with = "Option<Vec<String>>",
444            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
445        )
446    )]
447    #[option(
448        default = "[]",
449        value_type = "list[str]",
450        example = r#"
451            dev-dependencies = ["ruff==0.5.0"]
452        "#
453    )]
454    pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
455
456    /// Overrides to apply when resolving the project's dependencies.
457    ///
458    /// Overrides are used to force selection of a specific version of a package, regardless of the
459    /// version requested by any other package, and regardless of whether choosing that version
460    /// would typically constitute an invalid resolution.
461    ///
462    /// While constraints are _additive_, in that they're combined with the requirements of the
463    /// constituent packages, overrides are _absolute_, in that they completely replace the
464    /// requirements of any constituent packages.
465    ///
466    /// Including a package as an override will _not_ trigger installation of the package on its
467    /// own; instead, the package must be requested elsewhere in the project's first-party or
468    /// transitive dependencies.
469    ///
470    /// !!! note
471    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `override-dependencies` from
472    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
473    ///     workspace members or `uv.toml` files.
474    #[cfg_attr(
475        feature = "schemars",
476        schemars(
477            with = "Option<Vec<String>>",
478            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
479        )
480    )]
481    #[option(
482        default = "[]",
483        value_type = "list[str]",
484        example = r#"
485            # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
486            # a different version.
487            override-dependencies = ["werkzeug==2.3.0"]
488        "#
489    )]
490    pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
491
492    /// Dependencies to exclude when resolving the project's dependencies.
493    ///
494    /// Excludes are used to prevent a package from being selected during resolution,
495    /// regardless of whether it's requested by any other package. When a package is excluded,
496    /// it will be omitted from the dependency list entirely.
497    ///
498    /// Including a package as an exclusion will prevent it from being installed, even if
499    /// it's requested by transitive dependencies. This can be useful for removing optional
500    /// dependencies or working around packages with broken dependencies.
501    ///
502    /// !!! note
503    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `exclude-dependencies` from
504    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
505    ///     workspace members or `uv.toml` files.
506    #[cfg_attr(
507        feature = "schemars",
508        schemars(
509            with = "Option<Vec<String>>",
510            description = "Package names to exclude, e.g., `werkzeug`, `numpy`."
511        )
512    )]
513    #[option(
514        default = "[]",
515        value_type = "list[str]",
516        example = r#"
517            # Exclude Werkzeug from being installed, even if transitive dependencies request it.
518            exclude-dependencies = ["werkzeug"]
519        "#
520    )]
521    pub exclude_dependencies: Option<Vec<PackageName>>,
522
523    /// Constraints to apply when resolving the project's dependencies.
524    ///
525    /// Constraints are used to restrict the versions of dependencies that are selected during
526    /// resolution.
527    ///
528    /// Including a package as a constraint will _not_ trigger installation of the package on its
529    /// own; instead, the package must be requested elsewhere in the project's first-party or
530    /// transitive dependencies.
531    ///
532    /// !!! note
533    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `constraint-dependencies` from
534    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
535    ///     workspace members or `uv.toml` files.
536    #[cfg_attr(
537        feature = "schemars",
538        schemars(
539            with = "Option<Vec<String>>",
540            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
541        )
542    )]
543    #[option(
544        default = "[]",
545        value_type = "list[str]",
546        example = r#"
547            # Ensure that the grpcio version is always less than 1.65, if it's requested by a
548            # direct or transitive dependency.
549            constraint-dependencies = ["grpcio<1.65"]
550        "#
551    )]
552    pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
553
554    /// Constraints to apply when solving build dependencies.
555    ///
556    /// Build constraints are used to restrict the versions of build dependencies that are selected
557    /// when building a package during resolution or installation.
558    ///
559    /// Including a package as a constraint will _not_ trigger installation of the package during
560    /// a build; instead, the package must be requested elsewhere in the project's build dependency
561    /// graph.
562    ///
563    /// !!! note
564    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `build-constraint-dependencies` from
565    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
566    ///     workspace members or `uv.toml` files.
567    #[cfg_attr(
568        feature = "schemars",
569        schemars(
570            with = "Option<Vec<String>>",
571            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
572        )
573    )]
574    #[option(
575        default = "[]",
576        value_type = "list[str]",
577        example = r#"
578            # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
579            # on setuptools.
580            build-constraint-dependencies = ["setuptools==60.0.0"]
581        "#
582    )]
583    pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
584
585    /// A list of supported environments against which to resolve dependencies.
586    ///
587    /// By default, uv will resolve for all possible environments during a `uv lock` operation.
588    /// However, you can restrict the set of supported environments to improve performance and avoid
589    /// unsatisfiable branches in the solution space.
590    ///
591    /// These environments will also be respected when `uv pip compile` is invoked with the
592    /// `--universal` flag.
593    #[cfg_attr(
594        feature = "schemars",
595        schemars(
596            with = "Option<Vec<String>>",
597            description = "A list of environment markers, e.g., `python_version >= '3.6'`."
598        )
599    )]
600    #[option(
601        default = "[]",
602        value_type = "str | list[str]",
603        example = r#"
604            # Resolve for macOS, but not for Linux or Windows.
605            environments = ["sys_platform == 'darwin'"]
606        "#
607    )]
608    pub environments: Option<SupportedEnvironments>,
609
610    /// A list of required platforms, for packages that lack source distributions.
611    ///
612    /// When a package does not have a source distribution, it's availability will be limited to
613    /// the platforms supported by its built distributions (wheels). For example, if a package only
614    /// publishes wheels for Linux, then it won't be installable on macOS or Windows.
615    ///
616    /// By default, uv requires each package to include at least one wheel that is compatible with
617    /// the designated Python version. The `required-environments` setting can be used to ensure that
618    /// the resulting resolution contains wheels for specific platforms, or fails if no such wheels
619    /// are available.
620    ///
621    /// While the `environments` setting _limits_ the set of environments that uv will consider when
622    /// resolving dependencies, `required-environments` _expands_ the set of platforms that uv _must_
623    /// support when resolving dependencies.
624    ///
625    /// For example, `environments = ["sys_platform == 'darwin'"]` would limit uv to solving for
626    /// macOS (and ignoring Linux and Windows). On the other hand, `required-environments = ["sys_platform == 'darwin'"]`
627    /// would _require_ that any package without a source distribution include a wheel for macOS in
628    /// order to be installable.
629    #[cfg_attr(
630        feature = "schemars",
631        schemars(
632            with = "Option<Vec<String>>",
633            description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
634        )
635    )]
636    #[option(
637        default = "[]",
638        value_type = "str | list[str]",
639        example = r#"
640            # Require that the package is available for macOS ARM and x86 (Intel).
641            required-environments = [
642                "sys_platform == 'darwin' and platform_machine == 'arm64'",
643                "sys_platform == 'darwin' and platform_machine == 'x86_64'",
644            ]
645        "#
646    )]
647    pub required_environments: Option<SupportedEnvironments>,
648
649    /// Declare collections of extras or dependency groups that are conflicting
650    /// (i.e., mutually exclusive).
651    ///
652    /// It's useful to declare conflicts when two or more extras have mutually
653    /// incompatible dependencies. For example, extra `foo` might depend
654    /// on `numpy==2.0.0` while extra `bar` depends on `numpy==2.1.0`. While these
655    /// dependencies conflict, it may be the case that users are not expected to
656    /// activate both `foo` and `bar` at the same time, making it possible to
657    /// generate a universal resolution for the project despite the incompatibility.
658    ///
659    /// By making such conflicts explicit, uv can generate a universal resolution
660    /// for a project, taking into account that certain combinations of extras and
661    /// groups are mutually exclusive. In exchange, installation will fail if a
662    /// user attempts to activate both conflicting extras.
663    #[cfg_attr(
664        feature = "schemars",
665        schemars(description = "A list of sets of conflicting groups or extras.")
666    )]
667    #[option(
668        default = r#"[]"#,
669        value_type = "list[list[dict]]",
670        example = r#"
671            # Require that `package[extra1]` and `package[extra2]` are resolved
672            # in different forks so that they cannot conflict with one another.
673            conflicts = [
674                [
675                    { extra = "extra1" },
676                    { extra = "extra2" },
677                ]
678            ]
679
680            # Require that the dependency groups `group1` and `group2`
681            # are resolved in different forks so that they cannot conflict
682            # with one another.
683            conflicts = [
684                [
685                    { group = "group1" },
686                    { group = "group2" },
687                ]
688            ]
689        "#
690    )]
691    pub conflicts: Option<SchemaConflicts>,
692
693    // Only exists on this type for schema and docs generation, the build backend settings are
694    // never merged in a workspace and read separately by the backend code.
695    /// Configuration for the uv build backend.
696    ///
697    /// Note that those settings only apply when using the `uv_build` backend, other build backends
698    /// (such as hatchling) have their own configuration.
699    #[option_group]
700    pub build_backend: Option<BuildBackendSettingsSchema>,
701}
702
703#[derive(Default, Debug, Clone, PartialEq, Eq)]
704#[cfg_attr(test, derive(Serialize))]
705#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
706pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
707
708impl ToolUvSources {
709    /// Returns the underlying `BTreeMap` of package names to sources.
710    pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
711        &self.0
712    }
713
714    /// Convert the [`ToolUvSources`] into its inner `BTreeMap`.
715    #[must_use]
716    pub fn into_inner(self) -> BTreeMap<PackageName, Sources> {
717        self.0
718    }
719}
720
721/// Ensure that all keys in the TOML table are unique.
722impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
723    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
724    where
725        D: Deserializer<'de>,
726    {
727        deserialize_unique_map(deserializer, |key: &PackageName| {
728            format!("duplicate sources for package `{key}`")
729        })
730        .map(ToolUvSources)
731    }
732}
733
734#[derive(Default, Debug, Clone, PartialEq, Eq)]
735#[cfg_attr(test, derive(Serialize))]
736#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
737pub struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
738
739impl ToolUvDependencyGroups {
740    /// Returns the underlying `BTreeMap` of group names to settings.
741    pub fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
742        &self.0
743    }
744
745    /// Convert the [`ToolUvDependencyGroups`] into its inner `BTreeMap`.
746    #[must_use]
747    pub fn into_inner(self) -> BTreeMap<GroupName, DependencyGroupSettings> {
748        self.0
749    }
750}
751
752/// Ensure that all keys in the TOML table are unique.
753impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
754    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
755    where
756        D: Deserializer<'de>,
757    {
758        deserialize_unique_map(deserializer, |key: &GroupName| {
759            format!("duplicate settings for dependency group `{key}`")
760        })
761        .map(ToolUvDependencyGroups)
762    }
763}
764
765#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
766#[cfg_attr(test, derive(Serialize))]
767#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
768#[serde(rename_all = "kebab-case")]
769pub struct DependencyGroupSettings {
770    /// Version of python to require when installing this group
771    #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
772    pub requires_python: Option<VersionSpecifiers>,
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
776#[serde(untagged, rename_all = "kebab-case")]
777#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
778pub enum ExtraBuildDependencyWire {
779    Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
780    #[serde(rename_all = "kebab-case")]
781    Annotated {
782        requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
783        match_runtime: bool,
784    },
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
788#[serde(
789    deny_unknown_fields,
790    from = "ExtraBuildDependencyWire",
791    into = "ExtraBuildDependencyWire"
792)]
793#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
794pub struct ExtraBuildDependency {
795    pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
796    pub match_runtime: bool,
797}
798
799impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
800    fn from(value: ExtraBuildDependency) -> Self {
801        value.requirement
802    }
803}
804
805impl From<ExtraBuildDependencyWire> for ExtraBuildDependency {
806    fn from(wire: ExtraBuildDependencyWire) -> Self {
807        match wire {
808            ExtraBuildDependencyWire::Unannotated(requirement) => Self {
809                requirement,
810                match_runtime: false,
811            },
812            ExtraBuildDependencyWire::Annotated {
813                requirement,
814                match_runtime,
815            } => Self {
816                requirement,
817                match_runtime,
818            },
819        }
820    }
821}
822
823impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
824    fn from(item: ExtraBuildDependency) -> Self {
825        Self::Annotated {
826            requirement: item.requirement,
827            match_runtime: item.match_runtime,
828        }
829    }
830}
831
832#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
833#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
834pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
835
836impl std::ops::Deref for ExtraBuildDependencies {
837    type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
838
839    fn deref(&self) -> &Self::Target {
840        &self.0
841    }
842}
843
844impl std::ops::DerefMut for ExtraBuildDependencies {
845    fn deref_mut(&mut self) -> &mut Self::Target {
846        &mut self.0
847    }
848}
849
850impl IntoIterator for ExtraBuildDependencies {
851    type Item = (PackageName, Vec<ExtraBuildDependency>);
852    type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
853
854    fn into_iter(self) -> Self::IntoIter {
855        self.0.into_iter()
856    }
857}
858
859impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
860    fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
861        iter: T,
862    ) -> Self {
863        Self(iter.into_iter().collect())
864    }
865}
866
867/// Ensure that all keys in the TOML table are unique.
868impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
869    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
870    where
871        D: Deserializer<'de>,
872    {
873        deserialize_unique_map(deserializer, |key: &PackageName| {
874            format!("duplicate extra-build-dependencies for `{key}`")
875        })
876        .map(ExtraBuildDependencies)
877    }
878}
879
880#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
881#[cfg_attr(test, derive(Serialize))]
882#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
883#[serde(rename_all = "kebab-case", deny_unknown_fields)]
884pub struct ToolUvWorkspace {
885    /// Packages to include as workspace members.
886    ///
887    /// Supports both globs and explicit paths.
888    ///
889    /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
890    #[option(
891        default = "[]",
892        value_type = "list[str]",
893        example = r#"
894            members = ["member1", "path/to/member2", "libs/*"]
895        "#
896    )]
897    pub members: Option<Vec<SerdePattern>>,
898    /// Packages to exclude as workspace members. If a package matches both `members` and
899    /// `exclude`, it will be excluded.
900    ///
901    /// Supports both globs and explicit paths.
902    ///
903    /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
904    #[option(
905        default = "[]",
906        value_type = "list[str]",
907        example = r#"
908            exclude = ["member1", "path/to/member2", "libs/*"]
909        "#
910    )]
911    pub exclude: Option<Vec<SerdePattern>>,
912}
913
914/// (De)serialize globs as strings.
915#[derive(Debug, Clone, PartialEq, Eq)]
916pub struct SerdePattern(Pattern);
917
918impl serde::ser::Serialize for SerdePattern {
919    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
920    where
921        S: serde::ser::Serializer,
922    {
923        self.0.as_str().serialize(serializer)
924    }
925}
926
927impl<'de> serde::Deserialize<'de> for SerdePattern {
928    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
929        struct Visitor;
930
931        impl serde::de::Visitor<'_> for Visitor {
932            type Value = SerdePattern;
933
934            fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
935                f.write_str("a string")
936            }
937
938            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
939                Pattern::from_str(v)
940                    .map(SerdePattern)
941                    .map_err(serde::de::Error::custom)
942            }
943        }
944
945        deserializer.deserialize_str(Visitor)
946    }
947}
948
949#[cfg(feature = "schemars")]
950impl schemars::JsonSchema for SerdePattern {
951    fn schema_name() -> Cow<'static, str> {
952        Cow::Borrowed("SerdePattern")
953    }
954
955    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
956        <String as schemars::JsonSchema>::json_schema(generator)
957    }
958}
959
960impl Deref for SerdePattern {
961    type Target = Pattern;
962
963    fn deref(&self) -> &Self::Target {
964        &self.0
965    }
966}
967
968#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
969#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
970#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
971pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
972
973impl Sources {
974    /// Return an [`Iterator`] over the sources.
975    ///
976    /// If the iterator contains multiple entries, they will always use disjoint markers.
977    ///
978    /// The iterator will contain at most one registry source.
979    pub fn iter(&self) -> impl Iterator<Item = &Source> {
980        self.0.iter()
981    }
982
983    /// Returns `true` if the sources list is empty.
984    pub fn is_empty(&self) -> bool {
985        self.0.is_empty()
986    }
987
988    /// Returns the number of sources in the list.
989    pub fn len(&self) -> usize {
990        self.0.len()
991    }
992}
993
994impl FromIterator<Source> for Sources {
995    fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
996        Self(iter.into_iter().collect())
997    }
998}
999
1000impl IntoIterator for Sources {
1001    type Item = Source;
1002    type IntoIter = std::vec::IntoIter<Source>;
1003
1004    fn into_iter(self) -> Self::IntoIter {
1005        self.0.into_iter()
1006    }
1007}
1008
1009#[derive(Debug, Clone, PartialEq, Eq)]
1010#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1011#[allow(clippy::large_enum_variant)]
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: Option<bool>,
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() || tag.is_some() || rev.is_some() || lfs.is_some())
1623        {
1624            if let Some(sources) = existing_sources {
1625                if let Some(package_sources) = sources.get(name) {
1626                    for existing_source in package_sources.iter() {
1627                        if let Self::Git {
1628                            git,
1629                            subdirectory,
1630                            marker,
1631                            extra,
1632                            group,
1633                            ..
1634                        } = existing_source
1635                        {
1636                            return Ok(Some(Self::Git {
1637                                git: git.clone(),
1638                                subdirectory: subdirectory.clone(),
1639                                rev,
1640                                tag,
1641                                branch,
1642                                lfs,
1643                                marker: *marker,
1644                                extra: extra.clone(),
1645                                group: group.clone(),
1646                            }));
1647                        }
1648                    }
1649                }
1650            }
1651            if let Some(rev) = rev {
1652                return Err(SourceError::UnusedRev(name.to_string(), rev));
1653            }
1654            if let Some(tag) = tag {
1655                return Err(SourceError::UnusedTag(name.to_string(), tag));
1656            }
1657            if let Some(branch) = branch {
1658                return Err(SourceError::UnusedBranch(name.to_string(), branch));
1659            }
1660            if let Some(true) = lfs {
1661                return Err(SourceError::UnusedLfs(name.to_string()));
1662            }
1663        }
1664
1665        // If we resolved a non-path source, and user specified an `--editable` flag, error.
1666        if !workspace {
1667            if !matches!(source, RequirementSource::Directory { .. }) {
1668                if editable == Some(true) {
1669                    return Err(SourceError::UnusedEditable(name.to_string()));
1670                }
1671            }
1672        }
1673
1674        // If the source is a workspace package, error if the user tried to specify a source.
1675        if workspace {
1676            return match source {
1677                RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1678                    Ok(Some(Self::Workspace {
1679                        workspace: true,
1680                        editable,
1681                        marker: MarkerTree::TRUE,
1682                        extra: None,
1683                        group: None,
1684                    }))
1685                }
1686                RequirementSource::Url { .. } => {
1687                    Err(SourceError::WorkspacePackageUrl(name.to_string()))
1688                }
1689                RequirementSource::Git { .. } => {
1690                    Err(SourceError::WorkspacePackageGit(name.to_string()))
1691                }
1692                RequirementSource::Path { .. } => {
1693                    Err(SourceError::WorkspacePackageFile(name.to_string()))
1694                }
1695            };
1696        }
1697
1698        let source = match source {
1699            RequirementSource::Registry { index: Some(_), .. } => {
1700                return Ok(None);
1701            }
1702            RequirementSource::Registry { index: None, .. } => {
1703                if let Some(index) = index {
1704                    Self::Registry {
1705                        index,
1706                        marker: MarkerTree::TRUE,
1707                        extra: None,
1708                        group: None,
1709                    }
1710                } else {
1711                    return Ok(None);
1712                }
1713            }
1714            RequirementSource::Path { install_path, .. } => Self::Path {
1715                editable: None,
1716                package: None,
1717                path: PortablePathBuf::from(
1718                    relative_to(&install_path, root)
1719                        .or_else(|_| std::path::absolute(&install_path))
1720                        .map_err(SourceError::Absolute)?
1721                        .into_boxed_path(),
1722                ),
1723                marker: MarkerTree::TRUE,
1724                extra: None,
1725                group: None,
1726            },
1727            RequirementSource::Directory {
1728                install_path,
1729                editable: is_editable,
1730                ..
1731            } => Self::Path {
1732                editable: editable.or(is_editable),
1733                package: None,
1734                path: PortablePathBuf::from(
1735                    relative_to(&install_path, root)
1736                        .or_else(|_| std::path::absolute(&install_path))
1737                        .map_err(SourceError::Absolute)?
1738                        .into_boxed_path(),
1739                ),
1740                marker: MarkerTree::TRUE,
1741                extra: None,
1742                group: None,
1743            },
1744            RequirementSource::Url {
1745                location,
1746                subdirectory,
1747                ..
1748            } => Self::Url {
1749                url: location,
1750                subdirectory: subdirectory.map(PortablePathBuf::from),
1751                marker: MarkerTree::TRUE,
1752                extra: None,
1753                group: None,
1754            },
1755            RequirementSource::Git {
1756                git, subdirectory, ..
1757            } => {
1758                if rev.is_none() && tag.is_none() && branch.is_none() {
1759                    let rev = match git.reference() {
1760                        GitReference::Branch(rev) => Some(rev),
1761                        GitReference::Tag(rev) => Some(rev),
1762                        GitReference::BranchOrTag(rev) => Some(rev),
1763                        GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1764                        GitReference::NamedRef(rev) => Some(rev),
1765                        GitReference::DefaultBranch => None,
1766                    };
1767                    Self::Git {
1768                        rev: rev.cloned(),
1769                        tag,
1770                        branch,
1771                        lfs,
1772                        git: git.repository().clone(),
1773                        subdirectory: subdirectory.map(PortablePathBuf::from),
1774                        marker: MarkerTree::TRUE,
1775                        extra: None,
1776                        group: None,
1777                    }
1778                } else {
1779                    Self::Git {
1780                        rev,
1781                        tag,
1782                        branch,
1783                        lfs,
1784                        git: git.repository().clone(),
1785                        subdirectory: subdirectory.map(PortablePathBuf::from),
1786                        marker: MarkerTree::TRUE,
1787                        extra: None,
1788                        group: None,
1789                    }
1790                }
1791            }
1792        };
1793
1794        Ok(Some(source))
1795    }
1796
1797    /// Return the [`MarkerTree`] for the source.
1798    pub fn marker(&self) -> MarkerTree {
1799        match self {
1800            Self::Git { marker, .. } => *marker,
1801            Self::Url { marker, .. } => *marker,
1802            Self::Path { marker, .. } => *marker,
1803            Self::Registry { marker, .. } => *marker,
1804            Self::Workspace { marker, .. } => *marker,
1805        }
1806    }
1807
1808    /// Return the extra name for the source.
1809    pub fn extra(&self) -> Option<&ExtraName> {
1810        match self {
1811            Self::Git { extra, .. } => extra.as_ref(),
1812            Self::Url { extra, .. } => extra.as_ref(),
1813            Self::Path { extra, .. } => extra.as_ref(),
1814            Self::Registry { extra, .. } => extra.as_ref(),
1815            Self::Workspace { extra, .. } => extra.as_ref(),
1816        }
1817    }
1818
1819    /// Return the dependency group name for the source.
1820    pub fn group(&self) -> Option<&GroupName> {
1821        match self {
1822            Self::Git { group, .. } => group.as_ref(),
1823            Self::Url { group, .. } => group.as_ref(),
1824            Self::Path { group, .. } => group.as_ref(),
1825            Self::Registry { group, .. } => group.as_ref(),
1826            Self::Workspace { group, .. } => group.as_ref(),
1827        }
1828    }
1829}
1830
1831/// The type of a dependency in a `pyproject.toml`.
1832#[derive(Debug, Clone, PartialEq, Eq)]
1833pub enum DependencyType {
1834    /// A dependency in `project.dependencies`.
1835    Production,
1836    /// A dependency in `tool.uv.dev-dependencies`.
1837    Dev,
1838    /// A dependency in `project.optional-dependencies.{0}`.
1839    Optional(ExtraName),
1840    /// A dependency in `dependency-groups.{0}`.
1841    Group(GroupName),
1842}
1843
1844#[derive(Debug, Clone, PartialEq, Eq)]
1845#[cfg_attr(test, derive(Serialize))]
1846pub struct BuildBackendSettingsSchema;
1847
1848impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1849    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1850    where
1851        D: Deserializer<'de>,
1852    {
1853        Ok(Self)
1854    }
1855}
1856
1857#[cfg(feature = "schemars")]
1858impl schemars::JsonSchema for BuildBackendSettingsSchema {
1859    fn schema_name() -> Cow<'static, str> {
1860        BuildBackendSettings::schema_name()
1861    }
1862
1863    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1864        BuildBackendSettings::json_schema(generator)
1865    }
1866}
1867
1868impl OptionsMetadata for BuildBackendSettingsSchema {
1869    fn record(visit: &mut dyn Visit) {
1870        BuildBackendSettings::record(visit);
1871    }
1872
1873    fn documentation() -> Option<&'static str> {
1874        BuildBackendSettings::documentation()
1875    }
1876
1877    fn metadata() -> OptionSet
1878    where
1879        Self: Sized + 'static,
1880    {
1881        BuildBackendSettings::metadata()
1882    }
1883}