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        #[serde(
1126            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1127            serialize_with = "uv_pep508::marker::ser::serialize",
1128            default
1129        )]
1130        marker: MarkerTree,
1131        extra: Option<ExtraName>,
1132        group: Option<GroupName>,
1133    },
1134    /// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution
1135    /// (`.zip`, `.tar.gz`).
1136    ///
1137    /// Example:
1138    /// ```toml
1139    /// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" }
1140    /// ```
1141    Url {
1142        url: DisplaySafeUrl,
1143        /// For source distributions, the path to the directory with the `pyproject.toml`, if it's
1144        /// not in the archive root.
1145        subdirectory: Option<PortablePathBuf>,
1146        #[serde(
1147            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1148            serialize_with = "uv_pep508::marker::ser::serialize",
1149            default
1150        )]
1151        marker: MarkerTree,
1152        extra: Option<ExtraName>,
1153        group: Option<GroupName>,
1154    },
1155    /// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
1156    /// `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
1157    /// `setup.py` file in the root).
1158    Path {
1159        path: PortablePathBuf,
1160        /// `false` by default.
1161        editable: Option<bool>,
1162        /// Whether to treat the dependency as a buildable Python package (`true`) or as a virtual
1163        /// package (`false`). If `false`, the package will not be built or installed, but its
1164        /// dependencies will be included in the virtual environment.
1165        ///
1166        /// When omitted, the package status is inferred based on the presence of a `[build-system]`
1167        /// in the project's `pyproject.toml`.
1168        package: Option<bool>,
1169        #[serde(
1170            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1171            serialize_with = "uv_pep508::marker::ser::serialize",
1172            default
1173        )]
1174        marker: MarkerTree,
1175        extra: Option<ExtraName>,
1176        group: Option<GroupName>,
1177    },
1178    /// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
1179    Registry {
1180        index: IndexName,
1181        #[serde(
1182            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1183            serialize_with = "uv_pep508::marker::ser::serialize",
1184            default
1185        )]
1186        marker: MarkerTree,
1187        extra: Option<ExtraName>,
1188        group: Option<GroupName>,
1189    },
1190    /// A dependency on another package in the workspace.
1191    Workspace {
1192        /// When set to `false`, the package will be fetched from the remote index, rather than
1193        /// included as a workspace package.
1194        workspace: bool,
1195        /// Whether the package should be installed as editable. Defaults to `true`.
1196        editable: Option<bool>,
1197        #[serde(
1198            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1199            serialize_with = "uv_pep508::marker::ser::serialize",
1200            default
1201        )]
1202        marker: MarkerTree,
1203        extra: Option<ExtraName>,
1204        group: Option<GroupName>,
1205    },
1206}
1207
1208/// A custom deserialization implementation for [`Source`]. This is roughly equivalent to
1209/// `#[serde(untagged)]`, but provides more detailed error messages.
1210impl<'de> Deserialize<'de> for Source {
1211    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1212    where
1213        D: Deserializer<'de>,
1214    {
1215        #[derive(Deserialize, Debug, Clone)]
1216        #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1217        struct CatchAll {
1218            git: Option<DisplaySafeUrl>,
1219            subdirectory: Option<PortablePathBuf>,
1220            rev: Option<String>,
1221            tag: Option<String>,
1222            branch: Option<String>,
1223            url: Option<DisplaySafeUrl>,
1224            path: Option<PortablePathBuf>,
1225            editable: Option<bool>,
1226            package: Option<bool>,
1227            index: Option<IndexName>,
1228            workspace: Option<bool>,
1229            #[serde(
1230                skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1231                serialize_with = "uv_pep508::marker::ser::serialize",
1232                default
1233            )]
1234            marker: MarkerTree,
1235            extra: Option<ExtraName>,
1236            group: Option<GroupName>,
1237        }
1238
1239        // Attempt to deserialize as `CatchAll`.
1240        let CatchAll {
1241            git,
1242            subdirectory,
1243            rev,
1244            tag,
1245            branch,
1246            url,
1247            path,
1248            editable,
1249            package,
1250            index,
1251            workspace,
1252            marker,
1253            extra,
1254            group,
1255        } = CatchAll::deserialize(deserializer)?;
1256
1257        // If both `extra` and `group` are set, return an error.
1258        if extra.is_some() && group.is_some() {
1259            return Err(serde::de::Error::custom(
1260                "cannot specify both `extra` and `group`",
1261            ));
1262        }
1263
1264        // If the `git` field is set, we're dealing with a Git source.
1265        if let Some(git) = git {
1266            if index.is_some() {
1267                return Err(serde::de::Error::custom(
1268                    "cannot specify both `git` and `index`",
1269                ));
1270            }
1271            if workspace.is_some() {
1272                return Err(serde::de::Error::custom(
1273                    "cannot specify both `git` and `workspace`",
1274                ));
1275            }
1276            if path.is_some() {
1277                return Err(serde::de::Error::custom(
1278                    "cannot specify both `git` and `path`",
1279                ));
1280            }
1281            if url.is_some() {
1282                return Err(serde::de::Error::custom(
1283                    "cannot specify both `git` and `url`",
1284                ));
1285            }
1286            if editable.is_some() {
1287                return Err(serde::de::Error::custom(
1288                    "cannot specify both `git` and `editable`",
1289                ));
1290            }
1291            if package.is_some() {
1292                return Err(serde::de::Error::custom(
1293                    "cannot specify both `git` and `package`",
1294                ));
1295            }
1296
1297            // At most one of `rev`, `tag`, or `branch` may be set.
1298            match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1299                (None, None, None) => {}
1300                (Some(_), None, None) => {}
1301                (None, Some(_), None) => {}
1302                (None, None, Some(_)) => {}
1303                _ => {
1304                    return Err(serde::de::Error::custom(
1305                        "expected at most one of `rev`, `tag`, or `branch`",
1306                    ));
1307                }
1308            }
1309
1310            // If the user prefixed the URL with `git+`, strip it.
1311            let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1312                DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1313            } else {
1314                git
1315            };
1316
1317            return Ok(Self::Git {
1318                git,
1319                subdirectory,
1320                rev,
1321                tag,
1322                branch,
1323                marker,
1324                extra,
1325                group,
1326            });
1327        }
1328
1329        // If the `url` field is set, we're dealing with a URL source.
1330        if let Some(url) = url {
1331            if index.is_some() {
1332                return Err(serde::de::Error::custom(
1333                    "cannot specify both `url` and `index`",
1334                ));
1335            }
1336            if workspace.is_some() {
1337                return Err(serde::de::Error::custom(
1338                    "cannot specify both `url` and `workspace`",
1339                ));
1340            }
1341            if path.is_some() {
1342                return Err(serde::de::Error::custom(
1343                    "cannot specify both `url` and `path`",
1344                ));
1345            }
1346            if git.is_some() {
1347                return Err(serde::de::Error::custom(
1348                    "cannot specify both `url` and `git`",
1349                ));
1350            }
1351            if rev.is_some() {
1352                return Err(serde::de::Error::custom(
1353                    "cannot specify both `url` and `rev`",
1354                ));
1355            }
1356            if tag.is_some() {
1357                return Err(serde::de::Error::custom(
1358                    "cannot specify both `url` and `tag`",
1359                ));
1360            }
1361            if branch.is_some() {
1362                return Err(serde::de::Error::custom(
1363                    "cannot specify both `url` and `branch`",
1364                ));
1365            }
1366            if editable.is_some() {
1367                return Err(serde::de::Error::custom(
1368                    "cannot specify both `url` and `editable`",
1369                ));
1370            }
1371            if package.is_some() {
1372                return Err(serde::de::Error::custom(
1373                    "cannot specify both `url` and `package`",
1374                ));
1375            }
1376
1377            return Ok(Self::Url {
1378                url,
1379                subdirectory,
1380                marker,
1381                extra,
1382                group,
1383            });
1384        }
1385
1386        // If the `path` field is set, we're dealing with a path source.
1387        if let Some(path) = path {
1388            if index.is_some() {
1389                return Err(serde::de::Error::custom(
1390                    "cannot specify both `path` and `index`",
1391                ));
1392            }
1393            if workspace.is_some() {
1394                return Err(serde::de::Error::custom(
1395                    "cannot specify both `path` and `workspace`",
1396                ));
1397            }
1398            if git.is_some() {
1399                return Err(serde::de::Error::custom(
1400                    "cannot specify both `path` and `git`",
1401                ));
1402            }
1403            if url.is_some() {
1404                return Err(serde::de::Error::custom(
1405                    "cannot specify both `path` and `url`",
1406                ));
1407            }
1408            if rev.is_some() {
1409                return Err(serde::de::Error::custom(
1410                    "cannot specify both `path` and `rev`",
1411                ));
1412            }
1413            if tag.is_some() {
1414                return Err(serde::de::Error::custom(
1415                    "cannot specify both `path` and `tag`",
1416                ));
1417            }
1418            if branch.is_some() {
1419                return Err(serde::de::Error::custom(
1420                    "cannot specify both `path` and `branch`",
1421                ));
1422            }
1423
1424            // A project must be packaged in order to be installed as editable.
1425            if editable == Some(true) && package == Some(false) {
1426                return Err(serde::de::Error::custom(
1427                    "cannot specify both `editable = true` and `package = false`",
1428                ));
1429            }
1430
1431            return Ok(Self::Path {
1432                path,
1433                editable,
1434                package,
1435                marker,
1436                extra,
1437                group,
1438            });
1439        }
1440
1441        // If the `index` field is set, we're dealing with a registry source.
1442        if let Some(index) = index {
1443            if workspace.is_some() {
1444                return Err(serde::de::Error::custom(
1445                    "cannot specify both `index` and `workspace`",
1446                ));
1447            }
1448            if git.is_some() {
1449                return Err(serde::de::Error::custom(
1450                    "cannot specify both `index` and `git`",
1451                ));
1452            }
1453            if url.is_some() {
1454                return Err(serde::de::Error::custom(
1455                    "cannot specify both `index` and `url`",
1456                ));
1457            }
1458            if path.is_some() {
1459                return Err(serde::de::Error::custom(
1460                    "cannot specify both `index` and `path`",
1461                ));
1462            }
1463            if rev.is_some() {
1464                return Err(serde::de::Error::custom(
1465                    "cannot specify both `index` and `rev`",
1466                ));
1467            }
1468            if tag.is_some() {
1469                return Err(serde::de::Error::custom(
1470                    "cannot specify both `index` and `tag`",
1471                ));
1472            }
1473            if branch.is_some() {
1474                return Err(serde::de::Error::custom(
1475                    "cannot specify both `index` and `branch`",
1476                ));
1477            }
1478            if editable.is_some() {
1479                return Err(serde::de::Error::custom(
1480                    "cannot specify both `index` and `editable`",
1481                ));
1482            }
1483            if package.is_some() {
1484                return Err(serde::de::Error::custom(
1485                    "cannot specify both `index` and `package`",
1486                ));
1487            }
1488
1489            return Ok(Self::Registry {
1490                index,
1491                marker,
1492                extra,
1493                group,
1494            });
1495        }
1496
1497        // If the `workspace` field is set, we're dealing with a workspace source.
1498        if let Some(workspace) = workspace {
1499            if index.is_some() {
1500                return Err(serde::de::Error::custom(
1501                    "cannot specify both `workspace` and `index`",
1502                ));
1503            }
1504            if git.is_some() {
1505                return Err(serde::de::Error::custom(
1506                    "cannot specify both `workspace` and `git`",
1507                ));
1508            }
1509            if url.is_some() {
1510                return Err(serde::de::Error::custom(
1511                    "cannot specify both `workspace` and `url`",
1512                ));
1513            }
1514            if path.is_some() {
1515                return Err(serde::de::Error::custom(
1516                    "cannot specify both `workspace` and `path`",
1517                ));
1518            }
1519            if rev.is_some() {
1520                return Err(serde::de::Error::custom(
1521                    "cannot specify both `workspace` and `rev`",
1522                ));
1523            }
1524            if tag.is_some() {
1525                return Err(serde::de::Error::custom(
1526                    "cannot specify both `workspace` and `tag`",
1527                ));
1528            }
1529            if branch.is_some() {
1530                return Err(serde::de::Error::custom(
1531                    "cannot specify both `workspace` and `branch`",
1532                ));
1533            }
1534            if package.is_some() {
1535                return Err(serde::de::Error::custom(
1536                    "cannot specify both `workspace` and `package`",
1537                ));
1538            }
1539
1540            return Ok(Self::Workspace {
1541                workspace,
1542                editable,
1543                marker,
1544                extra,
1545                group,
1546            });
1547        }
1548
1549        // If none of the fields are set, we're dealing with an error.
1550        Err(serde::de::Error::custom(
1551            "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1552        ))
1553    }
1554}
1555
1556#[derive(Error, Debug)]
1557pub enum SourceError {
1558    #[error("Failed to resolve Git reference: `{0}`")]
1559    UnresolvedReference(String),
1560    #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1561    WorkspacePackageGit(String),
1562    #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1563    WorkspacePackageUrl(String),
1564    #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1565    WorkspacePackageFile(String),
1566    #[error(
1567        "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1568    )]
1569    UnusedRev(String, String),
1570    #[error(
1571        "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1572    )]
1573    UnusedTag(String, String),
1574    #[error(
1575        "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1576    )]
1577    UnusedBranch(String, String),
1578    #[error(
1579        "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1580    )]
1581    UnusedEditable(String),
1582    #[error("Failed to resolve absolute path")]
1583    Absolute(#[from] std::io::Error),
1584    #[error("Path contains invalid characters: `{}`", _0.display())]
1585    NonUtf8Path(PathBuf),
1586    #[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()
1587    )]
1588    OverlappingMarkers(String, String, String),
1589    #[error(
1590        "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1591    )]
1592    MissingMarkers,
1593    #[error("Must provide at least one source")]
1594    EmptySources,
1595}
1596
1597impl Source {
1598    pub fn from_requirement(
1599        name: &PackageName,
1600        source: RequirementSource,
1601        workspace: bool,
1602        editable: Option<bool>,
1603        index: Option<IndexName>,
1604        rev: Option<String>,
1605        tag: Option<String>,
1606        branch: Option<String>,
1607        root: &Path,
1608        existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1609    ) -> Result<Option<Self>, SourceError> {
1610        // If the user specified a Git reference for a non-Git source, try existing Git sources before erroring.
1611        if !matches!(source, RequirementSource::Git { .. })
1612            && (branch.is_some() || tag.is_some() || rev.is_some())
1613        {
1614            if let Some(sources) = existing_sources {
1615                if let Some(package_sources) = sources.get(name) {
1616                    for existing_source in package_sources.iter() {
1617                        if let Self::Git {
1618                            git,
1619                            subdirectory,
1620                            marker,
1621                            extra,
1622                            group,
1623                            ..
1624                        } = existing_source
1625                        {
1626                            return Ok(Some(Self::Git {
1627                                git: git.clone(),
1628                                subdirectory: subdirectory.clone(),
1629                                rev,
1630                                tag,
1631                                branch,
1632                                marker: *marker,
1633                                extra: extra.clone(),
1634                                group: group.clone(),
1635                            }));
1636                        }
1637                    }
1638                }
1639            }
1640            if let Some(rev) = rev {
1641                return Err(SourceError::UnusedRev(name.to_string(), rev));
1642            }
1643            if let Some(tag) = tag {
1644                return Err(SourceError::UnusedTag(name.to_string(), tag));
1645            }
1646            if let Some(branch) = branch {
1647                return Err(SourceError::UnusedBranch(name.to_string(), branch));
1648            }
1649        }
1650
1651        // If we resolved a non-path source, and user specified an `--editable` flag, error.
1652        if !workspace {
1653            if !matches!(source, RequirementSource::Directory { .. }) {
1654                if editable == Some(true) {
1655                    return Err(SourceError::UnusedEditable(name.to_string()));
1656                }
1657            }
1658        }
1659
1660        // If the source is a workspace package, error if the user tried to specify a source.
1661        if workspace {
1662            return match source {
1663                RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1664                    Ok(Some(Self::Workspace {
1665                        workspace: true,
1666                        editable,
1667                        marker: MarkerTree::TRUE,
1668                        extra: None,
1669                        group: None,
1670                    }))
1671                }
1672                RequirementSource::Url { .. } => {
1673                    Err(SourceError::WorkspacePackageUrl(name.to_string()))
1674                }
1675                RequirementSource::Git { .. } => {
1676                    Err(SourceError::WorkspacePackageGit(name.to_string()))
1677                }
1678                RequirementSource::Path { .. } => {
1679                    Err(SourceError::WorkspacePackageFile(name.to_string()))
1680                }
1681            };
1682        }
1683
1684        let source = match source {
1685            RequirementSource::Registry { index: Some(_), .. } => {
1686                return Ok(None);
1687            }
1688            RequirementSource::Registry { index: None, .. } => {
1689                if let Some(index) = index {
1690                    Self::Registry {
1691                        index,
1692                        marker: MarkerTree::TRUE,
1693                        extra: None,
1694                        group: None,
1695                    }
1696                } else {
1697                    return Ok(None);
1698                }
1699            }
1700            RequirementSource::Path { install_path, .. } => Self::Path {
1701                editable: None,
1702                package: None,
1703                path: PortablePathBuf::from(
1704                    relative_to(&install_path, root)
1705                        .or_else(|_| std::path::absolute(&install_path))
1706                        .map_err(SourceError::Absolute)?
1707                        .into_boxed_path(),
1708                ),
1709                marker: MarkerTree::TRUE,
1710                extra: None,
1711                group: None,
1712            },
1713            RequirementSource::Directory {
1714                install_path,
1715                editable: is_editable,
1716                ..
1717            } => Self::Path {
1718                editable: editable.or(is_editable),
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::Url {
1731                location,
1732                subdirectory,
1733                ..
1734            } => Self::Url {
1735                url: location,
1736                subdirectory: subdirectory.map(PortablePathBuf::from),
1737                marker: MarkerTree::TRUE,
1738                extra: None,
1739                group: None,
1740            },
1741            RequirementSource::Git {
1742                git, subdirectory, ..
1743            } => {
1744                if rev.is_none() && tag.is_none() && branch.is_none() {
1745                    let rev = match git.reference() {
1746                        GitReference::Branch(rev) => Some(rev),
1747                        GitReference::Tag(rev) => Some(rev),
1748                        GitReference::BranchOrTag(rev) => Some(rev),
1749                        GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1750                        GitReference::NamedRef(rev) => Some(rev),
1751                        GitReference::DefaultBranch => None,
1752                    };
1753                    Self::Git {
1754                        rev: rev.cloned(),
1755                        tag,
1756                        branch,
1757                        git: git.repository().clone(),
1758                        subdirectory: subdirectory.map(PortablePathBuf::from),
1759                        marker: MarkerTree::TRUE,
1760                        extra: None,
1761                        group: None,
1762                    }
1763                } else {
1764                    Self::Git {
1765                        rev,
1766                        tag,
1767                        branch,
1768                        git: git.repository().clone(),
1769                        subdirectory: subdirectory.map(PortablePathBuf::from),
1770                        marker: MarkerTree::TRUE,
1771                        extra: None,
1772                        group: None,
1773                    }
1774                }
1775            }
1776        };
1777
1778        Ok(Some(source))
1779    }
1780
1781    /// Return the [`MarkerTree`] for the source.
1782    pub fn marker(&self) -> MarkerTree {
1783        match self {
1784            Self::Git { marker, .. } => *marker,
1785            Self::Url { marker, .. } => *marker,
1786            Self::Path { marker, .. } => *marker,
1787            Self::Registry { marker, .. } => *marker,
1788            Self::Workspace { marker, .. } => *marker,
1789        }
1790    }
1791
1792    /// Return the extra name for the source.
1793    pub fn extra(&self) -> Option<&ExtraName> {
1794        match self {
1795            Self::Git { extra, .. } => extra.as_ref(),
1796            Self::Url { extra, .. } => extra.as_ref(),
1797            Self::Path { extra, .. } => extra.as_ref(),
1798            Self::Registry { extra, .. } => extra.as_ref(),
1799            Self::Workspace { extra, .. } => extra.as_ref(),
1800        }
1801    }
1802
1803    /// Return the dependency group name for the source.
1804    pub fn group(&self) -> Option<&GroupName> {
1805        match self {
1806            Self::Git { group, .. } => group.as_ref(),
1807            Self::Url { group, .. } => group.as_ref(),
1808            Self::Path { group, .. } => group.as_ref(),
1809            Self::Registry { group, .. } => group.as_ref(),
1810            Self::Workspace { group, .. } => group.as_ref(),
1811        }
1812    }
1813}
1814
1815/// The type of a dependency in a `pyproject.toml`.
1816#[derive(Debug, Clone, PartialEq, Eq)]
1817pub enum DependencyType {
1818    /// A dependency in `project.dependencies`.
1819    Production,
1820    /// A dependency in `tool.uv.dev-dependencies`.
1821    Dev,
1822    /// A dependency in `project.optional-dependencies.{0}`.
1823    Optional(ExtraName),
1824    /// A dependency in `dependency-groups.{0}`.
1825    Group(GroupName),
1826}
1827
1828#[derive(Debug, Clone, PartialEq, Eq)]
1829#[cfg_attr(test, derive(Serialize))]
1830pub struct BuildBackendSettingsSchema;
1831
1832impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1833    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1834    where
1835        D: Deserializer<'de>,
1836    {
1837        Ok(Self)
1838    }
1839}
1840
1841#[cfg(feature = "schemars")]
1842impl schemars::JsonSchema for BuildBackendSettingsSchema {
1843    fn schema_name() -> Cow<'static, str> {
1844        BuildBackendSettings::schema_name()
1845    }
1846
1847    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1848        BuildBackendSettings::json_schema(generator)
1849    }
1850}
1851
1852impl OptionsMetadata for BuildBackendSettingsSchema {
1853    fn record(visit: &mut dyn Visit) {
1854        BuildBackendSettings::record(visit);
1855    }
1856
1857    fn documentation() -> Option<&'static str> {
1858        BuildBackendSettings::documentation()
1859    }
1860
1861    fn metadata() -> OptionSet
1862    where
1863        Self: Sized + 'static,
1864    {
1865        BuildBackendSettings::metadata()
1866    }
1867}