uv_workspace/
pyproject.rs

1//! Reads the following fields from `pyproject.toml`:
2//!
3//! * `project.{dependencies,optional-dependencies}`
4//! * `tool.uv.sources`
5//! * `tool.uv.workspace`
6//!
7//! Then lowers them into a dependency specification.
8
9#[cfg(feature = "schemars")]
10use std::borrow::Cow;
11use std::collections::BTreeMap;
12use std::fmt::Formatter;
13use std::ops::Deref;
14use std::path::{Path, PathBuf};
15use std::str::FromStr;
16
17use glob::Pattern;
18use owo_colors::OwoColorize;
19use rustc_hash::{FxBuildHasher, FxHashSet};
20use serde::de::{IntoDeserializer, SeqAccess};
21use serde::{Deserialize, Deserializer, Serialize};
22use thiserror::Error;
23
24use uv_build_backend::BuildBackendSettings;
25use uv_configuration::GitLfsSetting;
26use uv_distribution_types::{Index, IndexName, RequirementSource};
27use uv_fs::{PortablePathBuf, relative_to};
28use uv_git_types::GitReference;
29use uv_macros::OptionsMetadata;
30use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName};
31use uv_options_metadata::{OptionSet, OptionsMetadata, Visit};
32use uv_pep440::{Version, VersionSpecifiers};
33use uv_pep508::MarkerTree;
34use uv_pypi_types::{
35    Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl,
36};
37use uv_redacted::DisplaySafeUrl;
38
39#[derive(Error, Debug)]
40pub enum PyprojectTomlError {
41    #[error(transparent)]
42    TomlSyntax(#[from] toml_edit::TomlError),
43    #[error(transparent)]
44    TomlSchema(#[from] toml_edit::de::Error),
45    #[error(
46        "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
47    )]
48    MissingName,
49    #[error(
50        "`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list"
51    )]
52    MissingVersion,
53}
54
55/// Helper function to deserialize a map while ensuring all keys are unique.
56fn deserialize_unique_map<'de, D, K, V, F>(
57    deserializer: D,
58    error_msg: F,
59) -> Result<BTreeMap<K, V>, D::Error>
60where
61    D: Deserializer<'de>,
62    K: Deserialize<'de> + Ord + std::fmt::Display,
63    V: Deserialize<'de>,
64    F: FnOnce(&K) -> String,
65{
66    struct Visitor<K, V, F>(F, std::marker::PhantomData<(K, V)>);
67
68    impl<'de, K, V, F> serde::de::Visitor<'de> for Visitor<K, V, F>
69    where
70        K: Deserialize<'de> + Ord + std::fmt::Display,
71        V: Deserialize<'de>,
72        F: FnOnce(&K) -> String,
73    {
74        type Value = BTreeMap<K, V>;
75
76        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
77            formatter.write_str("a map with unique keys")
78        }
79
80        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
81        where
82            M: serde::de::MapAccess<'de>,
83        {
84            use std::collections::btree_map::Entry;
85
86            let mut map = BTreeMap::new();
87            while let Some((key, value)) = access.next_entry::<K, V>()? {
88                match map.entry(key) {
89                    Entry::Occupied(entry) => {
90                        return Err(serde::de::Error::custom((self.0)(entry.key())));
91                    }
92                    Entry::Vacant(entry) => {
93                        entry.insert(value);
94                    }
95                }
96            }
97            Ok(map)
98        }
99    }
100
101    deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData))
102}
103
104/// A `pyproject.toml` as specified in PEP 517.
105#[derive(Deserialize, Debug, Clone)]
106#[cfg_attr(test, derive(Serialize))]
107#[serde(rename_all = "kebab-case")]
108pub struct PyProjectToml {
109    /// PEP 621-compliant project metadata.
110    pub project: Option<Project>,
111    /// Tool-specific metadata.
112    pub tool: Option<Tool>,
113    /// Non-project dependency groups, as defined in PEP 735.
114    pub dependency_groups: Option<DependencyGroups>,
115    /// The raw unserialized document.
116    #[serde(skip)]
117    pub raw: String,
118
119    /// Used to determine whether a `build-system` section is present.
120    #[serde(default, skip_serializing)]
121    pub build_system: Option<serde::de::IgnoredAny>,
122}
123
124impl PyProjectToml {
125    /// Parse a `PyProjectToml` from a raw TOML string.
126    pub fn from_string(raw: String) -> Result<Self, PyprojectTomlError> {
127        let pyproject =
128            toml_edit::Document::from_str(&raw).map_err(PyprojectTomlError::TomlSyntax)?;
129        let pyproject = Self::deserialize(pyproject.into_deserializer())
130            .map_err(PyprojectTomlError::TomlSchema)?;
131        Ok(Self { raw, ..pyproject })
132    }
133
134    /// Returns `true` if the project should be considered a Python package, as opposed to a
135    /// non-package ("virtual") project.
136    pub fn is_package(&self, require_build_system: bool) -> bool {
137        // If `tool.uv.package` is set, defer to that explicit setting.
138        if let Some(is_package) = self.tool_uv_package() {
139            return is_package;
140        }
141
142        // Otherwise, a project is assumed to be a package if `build-system` is present.
143        self.build_system.is_some() || !require_build_system
144    }
145
146    /// Returns the value of `tool.uv.package` if set.
147    fn tool_uv_package(&self) -> Option<bool> {
148        self.tool
149            .as_ref()
150            .and_then(|tool| tool.uv.as_ref())
151            .and_then(|uv| uv.package)
152    }
153
154    /// Returns `true` if the project uses a dynamic version.
155    pub fn is_dynamic(&self) -> bool {
156        self.project
157            .as_ref()
158            .is_some_and(|project| project.version.is_none())
159    }
160
161    /// Returns whether the project manifest contains any script table.
162    pub fn has_scripts(&self) -> bool {
163        if let Some(ref project) = self.project {
164            project.gui_scripts.is_some() || project.scripts.is_some()
165        } else {
166            false
167        }
168    }
169
170    /// Returns the set of conflicts for the project.
171    pub fn conflicts(&self) -> Conflicts {
172        let empty = Conflicts::empty();
173        let Some(project) = self.project.as_ref() else {
174            return empty;
175        };
176        let Some(tool) = self.tool.as_ref() else {
177            return empty;
178        };
179        let Some(tooluv) = tool.uv.as_ref() else {
180            return empty;
181        };
182        let Some(conflicting) = tooluv.conflicts.as_ref() else {
183            return empty;
184        };
185        conflicting.to_conflicts_with_package_name(&project.name)
186    }
187}
188
189// Ignore raw document in comparison.
190impl PartialEq for PyProjectToml {
191    fn eq(&self, other: &Self) -> bool {
192        self.project.eq(&other.project) && self.tool.eq(&other.tool)
193    }
194}
195
196impl Eq for PyProjectToml {}
197
198impl AsRef<[u8]> for PyProjectToml {
199    fn as_ref(&self) -> &[u8] {
200        self.raw.as_bytes()
201    }
202}
203
204/// PEP 621 project metadata (`project`).
205///
206/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
207#[derive(Deserialize, Debug, Clone, PartialEq)]
208#[cfg_attr(test, derive(Serialize))]
209#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
210pub struct Project {
211    /// The name of the project
212    pub name: PackageName,
213    /// The version of the project
214    pub version: Option<Version>,
215    /// The Python versions this project is compatible with.
216    pub requires_python: Option<VersionSpecifiers>,
217    /// The dependencies of the project.
218    pub dependencies: Option<Vec<String>>,
219    /// The optional dependencies of the project.
220    pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
221
222    /// Used to determine whether a `gui-scripts` section is present.
223    #[serde(default, skip_serializing)]
224    pub(crate) gui_scripts: Option<serde::de::IgnoredAny>,
225    /// Used to determine whether a `scripts` section is present.
226    #[serde(default, skip_serializing)]
227    pub(crate) scripts: Option<serde::de::IgnoredAny>,
228}
229
230#[derive(Deserialize, Debug)]
231#[serde(rename_all = "kebab-case")]
232struct ProjectWire {
233    name: Option<PackageName>,
234    version: Option<Version>,
235    dynamic: Option<Vec<String>>,
236    requires_python: Option<VersionSpecifiers>,
237    dependencies: Option<Vec<String>>,
238    optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
239    gui_scripts: Option<serde::de::IgnoredAny>,
240    scripts: Option<serde::de::IgnoredAny>,
241}
242
243impl TryFrom<ProjectWire> for Project {
244    type Error = PyprojectTomlError;
245
246    fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
247        // If `[project.name]` is not present, show a dedicated error message.
248        let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
249
250        // If `[project.version]` is not present (or listed in `[project.dynamic]`), show a dedicated error message.
251        if value.version.is_none()
252            && !value
253                .dynamic
254                .as_ref()
255                .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
256        {
257            return Err(PyprojectTomlError::MissingVersion);
258        }
259
260        Ok(Self {
261            name,
262            version: value.version,
263            requires_python: value.requires_python,
264            dependencies: value.dependencies,
265            optional_dependencies: value.optional_dependencies,
266            gui_scripts: value.gui_scripts,
267            scripts: value.scripts,
268        })
269    }
270}
271
272#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
273#[cfg_attr(test, derive(Serialize))]
274#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
275pub struct Tool {
276    pub uv: Option<ToolUv>,
277}
278
279/// Validates that index names in the `tool.uv.index` field are unique.
280///
281/// This custom deserializer function checks for duplicate index names
282/// and returns an error if any duplicates are found.
283fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
284where
285    D: Deserializer<'de>,
286{
287    let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
288    if let Some(indexes) = indexes.as_ref() {
289        let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
290        for index in indexes {
291            if let Some(name) = index.name.as_ref() {
292                if !seen_names.insert(name) {
293                    return Err(serde::de::Error::custom(format!(
294                        "duplicate index name `{name}`"
295                    )));
296                }
297            }
298        }
299    }
300    Ok(indexes)
301}
302
303// NOTE(charlie): When adding fields to this struct, mark them as ignored on `Options` in
304// `crates/uv-settings/src/settings.rs`.
305#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
306#[cfg_attr(test, derive(Serialize))]
307#[serde(rename_all = "kebab-case")]
308#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
309pub struct ToolUv {
310    /// The sources to use when resolving dependencies.
311    ///
312    /// `tool.uv.sources` enriches the dependency metadata with additional sources, incorporated
313    /// during development. A dependency source can be a Git repository, a URL, a local path, or an
314    /// alternative registry.
315    ///
316    /// See [Dependencies](../concepts/projects/dependencies.md) for more.
317    #[option(
318        default = "{}",
319        value_type = "dict",
320        example = r#"
321            [tool.uv.sources]
322            httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
323            pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
324            pydantic = { path = "/path/to/pydantic", editable = true }
325        "#
326    )]
327    pub sources: Option<ToolUvSources>,
328
329    /// The indexes to use when resolving dependencies.
330    ///
331    /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)
332    /// (the simple repository API), or a local directory laid out in the same format.
333    ///
334    /// Indexes are considered in the order in which they're defined, such that the first-defined
335    /// index has the highest priority. Further, the indexes provided by this setting are given
336    /// higher priority than any indexes specified via [`index_url`](#index-url) or
337    /// [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains
338    /// a given package, unless an alternative [index strategy](#index-strategy) is specified.
339    ///
340    /// If an index is marked as `explicit = true`, it will be used exclusively for the
341    /// dependencies that select it explicitly via `[tool.uv.sources]`, as in:
342    ///
343    /// ```toml
344    /// [[tool.uv.index]]
345    /// name = "pytorch"
346    /// url = "https://download.pytorch.org/whl/cu121"
347    /// explicit = true
348    ///
349    /// [tool.uv.sources]
350    /// torch = { index = "pytorch" }
351    /// ```
352    ///
353    /// If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is
354    /// given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
355    /// PyPI default index.
356    #[option(
357        default = "[]",
358        value_type = "dict",
359        example = r#"
360            [[tool.uv.index]]
361            name = "pytorch"
362            url = "https://download.pytorch.org/whl/cu121"
363        "#
364    )]
365    #[serde(deserialize_with = "deserialize_index_vec", default)]
366    pub index: Option<Vec<Index>>,
367
368    /// The workspace definition for the project, if any.
369    #[option_group]
370    pub workspace: Option<ToolUvWorkspace>,
371
372    /// Whether the project is managed by uv. If `false`, uv will ignore the project when
373    /// `uv run` is invoked.
374    #[option(
375        default = r#"true"#,
376        value_type = "bool",
377        example = r#"
378            managed = false
379        "#
380    )]
381    pub managed: Option<bool>,
382
383    /// Whether the project should be considered a Python package, or a non-package ("virtual")
384    /// project.
385    ///
386    /// Packages are built and installed into the virtual environment in editable mode and thus
387    /// require a build backend, while virtual projects are _not_ built or installed; instead, only
388    /// their dependencies are included in the virtual environment.
389    ///
390    /// Creating a package requires that a `build-system` is present in the `pyproject.toml`, and
391    /// that the project adheres to a structure that adheres to the build backend's expectations
392    /// (e.g., a `src` layout).
393    #[option(
394        default = r#"true"#,
395        value_type = "bool",
396        example = r#"
397            package = false
398        "#
399    )]
400    pub package: Option<bool>,
401
402    /// The list of `dependency-groups` to install by default.
403    ///
404    /// Can also be the literal `"all"` to default enable all groups.
405    #[option(
406        default = r#"["dev"]"#,
407        value_type = r#"str | list[str]"#,
408        example = r#"
409            default-groups = ["docs"]
410        "#
411    )]
412    pub default_groups: Option<DefaultGroups>,
413
414    /// Additional settings for `dependency-groups`.
415    ///
416    /// Currently this can only be used to add `requires-python` constraints
417    /// to dependency groups (typically to inform uv that your dev tooling
418    /// has a higher python requirement than your actual project).
419    ///
420    /// This cannot be used to define dependency groups, use the top-level
421    /// `[dependency-groups]` table for that.
422    #[option(
423        default = "[]",
424        value_type = "dict",
425        example = r#"
426            [tool.uv.dependency-groups]
427            my-group = {requires-python = ">=3.12"}
428        "#
429    )]
430    pub dependency_groups: Option<ToolUvDependencyGroups>,
431
432    /// The project's development dependencies.
433    ///
434    /// Development dependencies will be installed by default in `uv run` and `uv sync`, but will
435    /// not appear in the project's published metadata.
436    ///
437    /// Use of this field is not recommend anymore. Instead, use the `dependency-groups.dev` field
438    /// which is a standardized way to declare development dependencies. The contents of
439    /// `tool.uv.dev-dependencies` and `dependency-groups.dev` are combined to determine the final
440    /// requirements of the `dev` dependency group.
441    #[cfg_attr(
442        feature = "schemars",
443        schemars(
444            with = "Option<Vec<String>>",
445            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
446        )
447    )]
448    #[option(
449        default = "[]",
450        value_type = "list[str]",
451        example = r#"
452            dev-dependencies = ["ruff==0.5.0"]
453        "#
454    )]
455    pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
456
457    /// Overrides to apply when resolving the project's dependencies.
458    ///
459    /// Overrides are used to force selection of a specific version of a package, regardless of the
460    /// version requested by any other package, and regardless of whether choosing that version
461    /// would typically constitute an invalid resolution.
462    ///
463    /// While constraints are _additive_, in that they're combined with the requirements of the
464    /// constituent packages, overrides are _absolute_, in that they completely replace the
465    /// requirements of any constituent packages.
466    ///
467    /// Including a package as an override will _not_ trigger installation of the package on its
468    /// own; instead, the package must be requested elsewhere in the project's first-party or
469    /// transitive dependencies.
470    ///
471    /// !!! note
472    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `override-dependencies` from
473    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
474    ///     workspace members or `uv.toml` files.
475    #[cfg_attr(
476        feature = "schemars",
477        schemars(
478            with = "Option<Vec<String>>",
479            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
480        )
481    )]
482    #[option(
483        default = "[]",
484        value_type = "list[str]",
485        example = r#"
486            # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
487            # a different version.
488            override-dependencies = ["werkzeug==2.3.0"]
489        "#
490    )]
491    pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
492
493    /// Dependencies to exclude when resolving the project's dependencies.
494    ///
495    /// Excludes are used to prevent a package from being selected during resolution,
496    /// regardless of whether it's requested by any other package. When a package is excluded,
497    /// it will be omitted from the dependency list entirely.
498    ///
499    /// Including a package as an exclusion will prevent it from being installed, even if
500    /// it's requested by transitive dependencies. This can be useful for removing optional
501    /// dependencies or working around packages with broken dependencies.
502    ///
503    /// !!! note
504    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `exclude-dependencies` from
505    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
506    ///     workspace members or `uv.toml` files.
507    #[cfg_attr(
508        feature = "schemars",
509        schemars(
510            with = "Option<Vec<String>>",
511            description = "Package names to exclude, e.g., `werkzeug`, `numpy`."
512        )
513    )]
514    #[option(
515        default = "[]",
516        value_type = "list[str]",
517        example = r#"
518            # Exclude Werkzeug from being installed, even if transitive dependencies request it.
519            exclude-dependencies = ["werkzeug"]
520        "#
521    )]
522    pub exclude_dependencies: Option<Vec<PackageName>>,
523
524    /// Constraints to apply when resolving the project's dependencies.
525    ///
526    /// Constraints are used to restrict the versions of dependencies that are selected during
527    /// resolution.
528    ///
529    /// Including a package as a constraint will _not_ trigger installation of the package on its
530    /// own; instead, the package must be requested elsewhere in the project's first-party or
531    /// transitive dependencies.
532    ///
533    /// !!! note
534    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `constraint-dependencies` from
535    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
536    ///     workspace members or `uv.toml` files.
537    #[cfg_attr(
538        feature = "schemars",
539        schemars(
540            with = "Option<Vec<String>>",
541            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
542        )
543    )]
544    #[option(
545        default = "[]",
546        value_type = "list[str]",
547        example = r#"
548            # Ensure that the grpcio version is always less than 1.65, if it's requested by a
549            # direct or transitive dependency.
550            constraint-dependencies = ["grpcio<1.65"]
551        "#
552    )]
553    pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
554
555    /// Constraints to apply when solving build dependencies.
556    ///
557    /// Build constraints are used to restrict the versions of build dependencies that are selected
558    /// when building a package during resolution or installation.
559    ///
560    /// Including a package as a constraint will _not_ trigger installation of the package during
561    /// a build; instead, the package must be requested elsewhere in the project's build dependency
562    /// graph.
563    ///
564    /// !!! note
565    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `build-constraint-dependencies` from
566    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
567    ///     workspace members or `uv.toml` files.
568    #[cfg_attr(
569        feature = "schemars",
570        schemars(
571            with = "Option<Vec<String>>",
572            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
573        )
574    )]
575    #[option(
576        default = "[]",
577        value_type = "list[str]",
578        example = r#"
579            # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
580            # on setuptools.
581            build-constraint-dependencies = ["setuptools==60.0.0"]
582        "#
583    )]
584    pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
585
586    /// A list of supported environments against which to resolve dependencies.
587    ///
588    /// By default, uv will resolve for all possible environments during a `uv lock` operation.
589    /// However, you can restrict the set of supported environments to improve performance and avoid
590    /// unsatisfiable branches in the solution space.
591    ///
592    /// These environments will also be respected when `uv pip compile` is invoked with the
593    /// `--universal` flag.
594    #[cfg_attr(
595        feature = "schemars",
596        schemars(
597            with = "Option<Vec<String>>",
598            description = "A list of environment markers, e.g., `python_version >= '3.6'`."
599        )
600    )]
601    #[option(
602        default = "[]",
603        value_type = "str | list[str]",
604        example = r#"
605            # Resolve for macOS, but not for Linux or Windows.
606            environments = ["sys_platform == 'darwin'"]
607        "#
608    )]
609    pub environments: Option<SupportedEnvironments>,
610
611    /// A list of required platforms, for packages that lack source distributions.
612    ///
613    /// When a package does not have a source distribution, it's availability will be limited to
614    /// the platforms supported by its built distributions (wheels). For example, if a package only
615    /// publishes wheels for Linux, then it won't be installable on macOS or Windows.
616    ///
617    /// By default, uv requires each package to include at least one wheel that is compatible with
618    /// the designated Python version. The `required-environments` setting can be used to ensure that
619    /// the resulting resolution contains wheels for specific platforms, or fails if no such wheels
620    /// are available.
621    ///
622    /// While the `environments` setting _limits_ the set of environments that uv will consider when
623    /// resolving dependencies, `required-environments` _expands_ the set of platforms that uv _must_
624    /// support when resolving dependencies.
625    ///
626    /// For example, `environments = ["sys_platform == 'darwin'"]` would limit uv to solving for
627    /// macOS (and ignoring Linux and Windows). On the other hand, `required-environments = ["sys_platform == 'darwin'"]`
628    /// would _require_ that any package without a source distribution include a wheel for macOS in
629    /// order to be installable.
630    #[cfg_attr(
631        feature = "schemars",
632        schemars(
633            with = "Option<Vec<String>>",
634            description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
635        )
636    )]
637    #[option(
638        default = "[]",
639        value_type = "str | list[str]",
640        example = r#"
641            # Require that the package is available for macOS ARM and x86 (Intel).
642            required-environments = [
643                "sys_platform == 'darwin' and platform_machine == 'arm64'",
644                "sys_platform == 'darwin' and platform_machine == 'x86_64'",
645            ]
646        "#
647    )]
648    pub required_environments: Option<SupportedEnvironments>,
649
650    /// Declare collections of extras or dependency groups that are conflicting
651    /// (i.e., mutually exclusive).
652    ///
653    /// It's useful to declare conflicts when two or more extras have mutually
654    /// incompatible dependencies. For example, extra `foo` might depend
655    /// on `numpy==2.0.0` while extra `bar` depends on `numpy==2.1.0`. While these
656    /// dependencies conflict, it may be the case that users are not expected to
657    /// activate both `foo` and `bar` at the same time, making it possible to
658    /// generate a universal resolution for the project despite the incompatibility.
659    ///
660    /// By making such conflicts explicit, uv can generate a universal resolution
661    /// for a project, taking into account that certain combinations of extras and
662    /// groups are mutually exclusive. In exchange, installation will fail if a
663    /// user attempts to activate both conflicting extras.
664    #[cfg_attr(
665        feature = "schemars",
666        schemars(description = "A list of sets of conflicting groups or extras.")
667    )]
668    #[option(
669        default = r#"[]"#,
670        value_type = "list[list[dict]]",
671        example = r#"
672            # Require that `package[extra1]` and `package[extra2]` are resolved
673            # in different forks so that they cannot conflict with one another.
674            conflicts = [
675                [
676                    { extra = "extra1" },
677                    { extra = "extra2" },
678                ]
679            ]
680
681            # Require that the dependency groups `group1` and `group2`
682            # are resolved in different forks so that they cannot conflict
683            # with one another.
684            conflicts = [
685                [
686                    { group = "group1" },
687                    { group = "group2" },
688                ]
689            ]
690        "#
691    )]
692    pub conflicts: Option<SchemaConflicts>,
693
694    // Only exists on this type for schema and docs generation, the build backend settings are
695    // never merged in a workspace and read separately by the backend code.
696    /// Configuration for the uv build backend.
697    ///
698    /// Note that those settings only apply when using the `uv_build` backend, other build backends
699    /// (such as hatchling) have their own configuration.
700    #[option_group]
701    pub build_backend: Option<BuildBackendSettingsSchema>,
702}
703
704#[derive(Default, Debug, Clone, PartialEq, Eq)]
705#[cfg_attr(test, derive(Serialize))]
706#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
707pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
708
709impl ToolUvSources {
710    /// Returns the underlying `BTreeMap` of package names to sources.
711    pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
712        &self.0
713    }
714
715    /// Convert the [`ToolUvSources`] into its inner `BTreeMap`.
716    #[must_use]
717    pub fn into_inner(self) -> BTreeMap<PackageName, Sources> {
718        self.0
719    }
720}
721
722/// Ensure that all keys in the TOML table are unique.
723impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
724    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
725    where
726        D: Deserializer<'de>,
727    {
728        deserialize_unique_map(deserializer, |key: &PackageName| {
729            format!("duplicate sources for package `{key}`")
730        })
731        .map(ToolUvSources)
732    }
733}
734
735#[derive(Default, Debug, Clone, PartialEq, Eq)]
736#[cfg_attr(test, derive(Serialize))]
737#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
738pub struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
739
740impl ToolUvDependencyGroups {
741    /// Returns the underlying `BTreeMap` of group names to settings.
742    pub fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
743        &self.0
744    }
745
746    /// Convert the [`ToolUvDependencyGroups`] into its inner `BTreeMap`.
747    #[must_use]
748    pub fn into_inner(self) -> BTreeMap<GroupName, DependencyGroupSettings> {
749        self.0
750    }
751}
752
753/// Ensure that all keys in the TOML table are unique.
754impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
755    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
756    where
757        D: Deserializer<'de>,
758    {
759        deserialize_unique_map(deserializer, |key: &GroupName| {
760            format!("duplicate settings for dependency group `{key}`")
761        })
762        .map(ToolUvDependencyGroups)
763    }
764}
765
766#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
767#[cfg_attr(test, derive(Serialize))]
768#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
769#[serde(rename_all = "kebab-case")]
770pub struct DependencyGroupSettings {
771    /// Version of python to require when installing this group
772    #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
773    pub requires_python: Option<VersionSpecifiers>,
774}
775
776#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
777#[serde(untagged, rename_all = "kebab-case")]
778#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
779pub enum ExtraBuildDependencyWire {
780    Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
781    #[serde(rename_all = "kebab-case")]
782    Annotated {
783        requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
784        match_runtime: bool,
785    },
786}
787
788#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
789#[serde(
790    deny_unknown_fields,
791    from = "ExtraBuildDependencyWire",
792    into = "ExtraBuildDependencyWire"
793)]
794#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
795pub struct ExtraBuildDependency {
796    pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
797    pub match_runtime: bool,
798}
799
800impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
801    fn from(value: ExtraBuildDependency) -> Self {
802        value.requirement
803    }
804}
805
806impl From<ExtraBuildDependencyWire> for ExtraBuildDependency {
807    fn from(wire: ExtraBuildDependencyWire) -> Self {
808        match wire {
809            ExtraBuildDependencyWire::Unannotated(requirement) => Self {
810                requirement,
811                match_runtime: false,
812            },
813            ExtraBuildDependencyWire::Annotated {
814                requirement,
815                match_runtime,
816            } => Self {
817                requirement,
818                match_runtime,
819            },
820        }
821    }
822}
823
824impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
825    fn from(item: ExtraBuildDependency) -> Self {
826        Self::Annotated {
827            requirement: item.requirement,
828            match_runtime: item.match_runtime,
829        }
830    }
831}
832
833#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
834#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
835pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
836
837impl std::ops::Deref for ExtraBuildDependencies {
838    type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
839
840    fn deref(&self) -> &Self::Target {
841        &self.0
842    }
843}
844
845impl std::ops::DerefMut for ExtraBuildDependencies {
846    fn deref_mut(&mut self) -> &mut Self::Target {
847        &mut self.0
848    }
849}
850
851impl IntoIterator for ExtraBuildDependencies {
852    type Item = (PackageName, Vec<ExtraBuildDependency>);
853    type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
854
855    fn into_iter(self) -> Self::IntoIter {
856        self.0.into_iter()
857    }
858}
859
860impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
861    fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
862        iter: T,
863    ) -> Self {
864        Self(iter.into_iter().collect())
865    }
866}
867
868/// Ensure that all keys in the TOML table are unique.
869impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
870    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
871    where
872        D: Deserializer<'de>,
873    {
874        deserialize_unique_map(deserializer, |key: &PackageName| {
875            format!("duplicate extra-build-dependencies for `{key}`")
876        })
877        .map(ExtraBuildDependencies)
878    }
879}
880
881#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
882#[cfg_attr(test, derive(Serialize))]
883#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
884#[serde(rename_all = "kebab-case", deny_unknown_fields)]
885pub struct ToolUvWorkspace {
886    /// Packages to include as workspace members.
887    ///
888    /// Supports both globs and explicit paths.
889    ///
890    /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
891    #[option(
892        default = "[]",
893        value_type = "list[str]",
894        example = r#"
895            members = ["member1", "path/to/member2", "libs/*"]
896        "#
897    )]
898    pub members: Option<Vec<SerdePattern>>,
899    /// Packages to exclude as workspace members. If a package matches both `members` and
900    /// `exclude`, it will be excluded.
901    ///
902    /// Supports both globs and explicit paths.
903    ///
904    /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
905    #[option(
906        default = "[]",
907        value_type = "list[str]",
908        example = r#"
909            exclude = ["member1", "path/to/member2", "libs/*"]
910        "#
911    )]
912    pub exclude: Option<Vec<SerdePattern>>,
913}
914
915/// (De)serialize globs as strings.
916#[derive(Debug, Clone, PartialEq, Eq)]
917pub struct SerdePattern(Pattern);
918
919impl serde::ser::Serialize for SerdePattern {
920    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
921    where
922        S: serde::ser::Serializer,
923    {
924        self.0.as_str().serialize(serializer)
925    }
926}
927
928impl<'de> serde::Deserialize<'de> for SerdePattern {
929    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
930        struct Visitor;
931
932        impl serde::de::Visitor<'_> for Visitor {
933            type Value = SerdePattern;
934
935            fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
936                f.write_str("a string")
937            }
938
939            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
940                Pattern::from_str(v)
941                    .map(SerdePattern)
942                    .map_err(serde::de::Error::custom)
943            }
944        }
945
946        deserializer.deserialize_str(Visitor)
947    }
948}
949
950#[cfg(feature = "schemars")]
951impl schemars::JsonSchema for SerdePattern {
952    fn schema_name() -> Cow<'static, str> {
953        Cow::Borrowed("SerdePattern")
954    }
955
956    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
957        <String as schemars::JsonSchema>::json_schema(generator)
958    }
959}
960
961impl Deref for SerdePattern {
962    type Target = Pattern;
963
964    fn deref(&self) -> &Self::Target {
965        &self.0
966    }
967}
968
969#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
970#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
971#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
972pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
973
974impl Sources {
975    /// Return an [`Iterator`] over the sources.
976    ///
977    /// If the iterator contains multiple entries, they will always use disjoint markers.
978    ///
979    /// The iterator will contain at most one registry source.
980    pub fn iter(&self) -> impl Iterator<Item = &Source> {
981        self.0.iter()
982    }
983
984    /// Returns `true` if the sources list is empty.
985    pub fn is_empty(&self) -> bool {
986        self.0.is_empty()
987    }
988
989    /// Returns the number of sources in the list.
990    pub fn len(&self) -> usize {
991        self.0.len()
992    }
993}
994
995impl FromIterator<Source> for Sources {
996    fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
997        Self(iter.into_iter().collect())
998    }
999}
1000
1001impl IntoIterator for Sources {
1002    type Item = Source;
1003    type IntoIter = std::vec::IntoIter<Source>;
1004
1005    fn into_iter(self) -> Self::IntoIter {
1006        self.0.into_iter()
1007    }
1008}
1009
1010#[derive(Debug, Clone, PartialEq, Eq)]
1011#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1012#[allow(clippy::large_enum_variant)]
1013enum SourcesWire {
1014    One(Source),
1015    Many(Vec<Source>),
1016}
1017
1018impl<'de> serde::de::Deserialize<'de> for SourcesWire {
1019    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1020    where
1021        D: Deserializer<'de>,
1022    {
1023        struct Visitor;
1024
1025        impl<'de> serde::de::Visitor<'de> for Visitor {
1026            type Value = SourcesWire;
1027
1028            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1029                formatter.write_str("a single source (as a map) or list of sources")
1030            }
1031
1032            fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1033            where
1034                A: SeqAccess<'de>,
1035            {
1036                let sources = serde::de::Deserialize::deserialize(
1037                    serde::de::value::SeqAccessDeserializer::new(seq),
1038                )?;
1039                Ok(SourcesWire::Many(sources))
1040            }
1041
1042            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1043            where
1044                M: serde::de::MapAccess<'de>,
1045            {
1046                let source = serde::de::Deserialize::deserialize(
1047                    serde::de::value::MapAccessDeserializer::new(&mut map),
1048                )?;
1049                Ok(SourcesWire::One(source))
1050            }
1051        }
1052
1053        deserializer.deserialize_any(Visitor)
1054    }
1055}
1056
1057impl TryFrom<SourcesWire> for Sources {
1058    type Error = SourceError;
1059
1060    fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
1061        match wire {
1062            SourcesWire::One(source) => Ok(Self(vec![source])),
1063            SourcesWire::Many(sources) => {
1064                for (lhs, rhs) in sources.iter().zip(sources.iter().skip(1)) {
1065                    if lhs.extra() != rhs.extra() {
1066                        continue;
1067                    }
1068                    if lhs.group() != rhs.group() {
1069                        continue;
1070                    }
1071
1072                    let lhs = lhs.marker();
1073                    let rhs = rhs.marker();
1074                    if !lhs.is_disjoint(rhs) {
1075                        let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
1076                            return Err(SourceError::MissingMarkers);
1077                        };
1078
1079                        let Some(right) = rhs.contents().map(|contents| contents.to_string())
1080                        else {
1081                            return Err(SourceError::MissingMarkers);
1082                        };
1083
1084                        let mut hint = lhs.negate();
1085                        hint.and(rhs);
1086                        let hint = hint
1087                            .contents()
1088                            .map(|contents| contents.to_string())
1089                            .unwrap_or_else(|| "true".to_string());
1090
1091                        return Err(SourceError::OverlappingMarkers(left, right, hint));
1092                    }
1093                }
1094
1095                // Ensure that there is at least one source.
1096                if sources.is_empty() {
1097                    return Err(SourceError::EmptySources);
1098                }
1099
1100                Ok(Self(sources))
1101            }
1102        }
1103    }
1104}
1105
1106/// A `tool.uv.sources` value.
1107#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1108#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1109#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1110pub enum Source {
1111    /// A remote Git repository, available over HTTPS or SSH.
1112    ///
1113    /// Example:
1114    /// ```toml
1115    /// flask = { git = "https://github.com/pallets/flask", tag = "3.0.0" }
1116    /// ```
1117    Git {
1118        /// The repository URL (without the `git+` prefix).
1119        git: DisplaySafeUrl,
1120        /// The path to the directory with the `pyproject.toml`, if it's not in the archive root.
1121        subdirectory: Option<PortablePathBuf>,
1122        // Only one of the three may be used; we'll validate this later and emit a custom error.
1123        rev: Option<String>,
1124        tag: Option<String>,
1125        branch: Option<String>,
1126        /// Whether to use Git LFS when cloning the repository.
1127        lfs: Option<bool>,
1128        #[serde(
1129            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1130            serialize_with = "uv_pep508::marker::ser::serialize",
1131            default
1132        )]
1133        marker: MarkerTree,
1134        extra: Option<ExtraName>,
1135        group: Option<GroupName>,
1136    },
1137    /// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution
1138    /// (`.zip`, `.tar.gz`).
1139    ///
1140    /// Example:
1141    /// ```toml
1142    /// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" }
1143    /// ```
1144    Url {
1145        url: DisplaySafeUrl,
1146        /// For source distributions, the path to the directory with the `pyproject.toml`, if it's
1147        /// not in the archive root.
1148        subdirectory: Option<PortablePathBuf>,
1149        #[serde(
1150            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1151            serialize_with = "uv_pep508::marker::ser::serialize",
1152            default
1153        )]
1154        marker: MarkerTree,
1155        extra: Option<ExtraName>,
1156        group: Option<GroupName>,
1157    },
1158    /// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
1159    /// `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
1160    /// `setup.py` file in the root).
1161    Path {
1162        path: PortablePathBuf,
1163        /// `false` by default.
1164        editable: Option<bool>,
1165        /// Whether to treat the dependency as a buildable Python package (`true`) or as a virtual
1166        /// package (`false`). If `false`, the package will not be built or installed, but its
1167        /// dependencies will be included in the virtual environment.
1168        ///
1169        /// When omitted, the package status is inferred based on the presence of a `[build-system]`
1170        /// in the project's `pyproject.toml`.
1171        package: Option<bool>,
1172        #[serde(
1173            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1174            serialize_with = "uv_pep508::marker::ser::serialize",
1175            default
1176        )]
1177        marker: MarkerTree,
1178        extra: Option<ExtraName>,
1179        group: Option<GroupName>,
1180    },
1181    /// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
1182    Registry {
1183        index: IndexName,
1184        #[serde(
1185            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1186            serialize_with = "uv_pep508::marker::ser::serialize",
1187            default
1188        )]
1189        marker: MarkerTree,
1190        extra: Option<ExtraName>,
1191        group: Option<GroupName>,
1192    },
1193    /// A dependency on another package in the workspace.
1194    Workspace {
1195        /// When set to `false`, the package will be fetched from the remote index, rather than
1196        /// included as a workspace package.
1197        workspace: bool,
1198        /// Whether the package should be installed as editable. Defaults to `true`.
1199        editable: Option<bool>,
1200        #[serde(
1201            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1202            serialize_with = "uv_pep508::marker::ser::serialize",
1203            default
1204        )]
1205        marker: MarkerTree,
1206        extra: Option<ExtraName>,
1207        group: Option<GroupName>,
1208    },
1209}
1210
1211/// A custom deserialization implementation for [`Source`]. This is roughly equivalent to
1212/// `#[serde(untagged)]`, but provides more detailed error messages.
1213impl<'de> Deserialize<'de> for Source {
1214    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1215    where
1216        D: Deserializer<'de>,
1217    {
1218        #[derive(Deserialize, Debug, Clone)]
1219        #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1220        struct CatchAll {
1221            git: Option<DisplaySafeUrl>,
1222            subdirectory: Option<PortablePathBuf>,
1223            rev: Option<String>,
1224            tag: Option<String>,
1225            branch: Option<String>,
1226            lfs: Option<bool>,
1227            url: Option<DisplaySafeUrl>,
1228            path: Option<PortablePathBuf>,
1229            editable: Option<bool>,
1230            package: Option<bool>,
1231            index: Option<IndexName>,
1232            workspace: Option<bool>,
1233            #[serde(
1234                skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1235                serialize_with = "uv_pep508::marker::ser::serialize",
1236                default
1237            )]
1238            marker: MarkerTree,
1239            extra: Option<ExtraName>,
1240            group: Option<GroupName>,
1241        }
1242
1243        // Attempt to deserialize as `CatchAll`.
1244        let CatchAll {
1245            git,
1246            subdirectory,
1247            rev,
1248            tag,
1249            branch,
1250            lfs,
1251            url,
1252            path,
1253            editable,
1254            package,
1255            index,
1256            workspace,
1257            marker,
1258            extra,
1259            group,
1260        } = CatchAll::deserialize(deserializer)?;
1261
1262        // If both `extra` and `group` are set, return an error.
1263        if extra.is_some() && group.is_some() {
1264            return Err(serde::de::Error::custom(
1265                "cannot specify both `extra` and `group`",
1266            ));
1267        }
1268
1269        // If the `git` field is set, we're dealing with a Git source.
1270        if let Some(git) = git {
1271            if index.is_some() {
1272                return Err(serde::de::Error::custom(
1273                    "cannot specify both `git` and `index`",
1274                ));
1275            }
1276            if workspace.is_some() {
1277                return Err(serde::de::Error::custom(
1278                    "cannot specify both `git` and `workspace`",
1279                ));
1280            }
1281            if path.is_some() {
1282                return Err(serde::de::Error::custom(
1283                    "cannot specify both `git` and `path`",
1284                ));
1285            }
1286            if url.is_some() {
1287                return Err(serde::de::Error::custom(
1288                    "cannot specify both `git` and `url`",
1289                ));
1290            }
1291            if editable.is_some() {
1292                return Err(serde::de::Error::custom(
1293                    "cannot specify both `git` and `editable`",
1294                ));
1295            }
1296            if package.is_some() {
1297                return Err(serde::de::Error::custom(
1298                    "cannot specify both `git` and `package`",
1299                ));
1300            }
1301
1302            // At most one of `rev`, `tag`, or `branch` may be set.
1303            match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1304                (None, None, None) => {}
1305                (Some(_), None, None) => {}
1306                (None, Some(_), None) => {}
1307                (None, None, Some(_)) => {}
1308                _ => {
1309                    return Err(serde::de::Error::custom(
1310                        "expected at most one of `rev`, `tag`, or `branch`",
1311                    ));
1312                }
1313            }
1314
1315            // If the user prefixed the URL with `git+`, strip it.
1316            let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1317                DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1318            } else {
1319                git
1320            };
1321
1322            return Ok(Self::Git {
1323                git,
1324                subdirectory,
1325                rev,
1326                tag,
1327                branch,
1328                lfs,
1329                marker,
1330                extra,
1331                group,
1332            });
1333        }
1334
1335        // If the `url` field is set, we're dealing with a URL source.
1336        if let Some(url) = url {
1337            if index.is_some() {
1338                return Err(serde::de::Error::custom(
1339                    "cannot specify both `url` and `index`",
1340                ));
1341            }
1342            if workspace.is_some() {
1343                return Err(serde::de::Error::custom(
1344                    "cannot specify both `url` and `workspace`",
1345                ));
1346            }
1347            if path.is_some() {
1348                return Err(serde::de::Error::custom(
1349                    "cannot specify both `url` and `path`",
1350                ));
1351            }
1352            if git.is_some() {
1353                return Err(serde::de::Error::custom(
1354                    "cannot specify both `url` and `git`",
1355                ));
1356            }
1357            if rev.is_some() {
1358                return Err(serde::de::Error::custom(
1359                    "cannot specify both `url` and `rev`",
1360                ));
1361            }
1362            if tag.is_some() {
1363                return Err(serde::de::Error::custom(
1364                    "cannot specify both `url` and `tag`",
1365                ));
1366            }
1367            if branch.is_some() {
1368                return Err(serde::de::Error::custom(
1369                    "cannot specify both `url` and `branch`",
1370                ));
1371            }
1372            if editable.is_some() {
1373                return Err(serde::de::Error::custom(
1374                    "cannot specify both `url` and `editable`",
1375                ));
1376            }
1377            if package.is_some() {
1378                return Err(serde::de::Error::custom(
1379                    "cannot specify both `url` and `package`",
1380                ));
1381            }
1382
1383            return Ok(Self::Url {
1384                url,
1385                subdirectory,
1386                marker,
1387                extra,
1388                group,
1389            });
1390        }
1391
1392        // If the `path` field is set, we're dealing with a path source.
1393        if let Some(path) = path {
1394            if index.is_some() {
1395                return Err(serde::de::Error::custom(
1396                    "cannot specify both `path` and `index`",
1397                ));
1398            }
1399            if workspace.is_some() {
1400                return Err(serde::de::Error::custom(
1401                    "cannot specify both `path` and `workspace`",
1402                ));
1403            }
1404            if git.is_some() {
1405                return Err(serde::de::Error::custom(
1406                    "cannot specify both `path` and `git`",
1407                ));
1408            }
1409            if url.is_some() {
1410                return Err(serde::de::Error::custom(
1411                    "cannot specify both `path` and `url`",
1412                ));
1413            }
1414            if rev.is_some() {
1415                return Err(serde::de::Error::custom(
1416                    "cannot specify both `path` and `rev`",
1417                ));
1418            }
1419            if tag.is_some() {
1420                return Err(serde::de::Error::custom(
1421                    "cannot specify both `path` and `tag`",
1422                ));
1423            }
1424            if branch.is_some() {
1425                return Err(serde::de::Error::custom(
1426                    "cannot specify both `path` and `branch`",
1427                ));
1428            }
1429
1430            // A project must be packaged in order to be installed as editable.
1431            if editable == Some(true) && package == Some(false) {
1432                return Err(serde::de::Error::custom(
1433                    "cannot specify both `editable = true` and `package = false`",
1434                ));
1435            }
1436
1437            return Ok(Self::Path {
1438                path,
1439                editable,
1440                package,
1441                marker,
1442                extra,
1443                group,
1444            });
1445        }
1446
1447        // If the `index` field is set, we're dealing with a registry source.
1448        if let Some(index) = index {
1449            if workspace.is_some() {
1450                return Err(serde::de::Error::custom(
1451                    "cannot specify both `index` and `workspace`",
1452                ));
1453            }
1454            if git.is_some() {
1455                return Err(serde::de::Error::custom(
1456                    "cannot specify both `index` and `git`",
1457                ));
1458            }
1459            if url.is_some() {
1460                return Err(serde::de::Error::custom(
1461                    "cannot specify both `index` and `url`",
1462                ));
1463            }
1464            if path.is_some() {
1465                return Err(serde::de::Error::custom(
1466                    "cannot specify both `index` and `path`",
1467                ));
1468            }
1469            if rev.is_some() {
1470                return Err(serde::de::Error::custom(
1471                    "cannot specify both `index` and `rev`",
1472                ));
1473            }
1474            if tag.is_some() {
1475                return Err(serde::de::Error::custom(
1476                    "cannot specify both `index` and `tag`",
1477                ));
1478            }
1479            if branch.is_some() {
1480                return Err(serde::de::Error::custom(
1481                    "cannot specify both `index` and `branch`",
1482                ));
1483            }
1484            if editable.is_some() {
1485                return Err(serde::de::Error::custom(
1486                    "cannot specify both `index` and `editable`",
1487                ));
1488            }
1489            if package.is_some() {
1490                return Err(serde::de::Error::custom(
1491                    "cannot specify both `index` and `package`",
1492                ));
1493            }
1494
1495            return Ok(Self::Registry {
1496                index,
1497                marker,
1498                extra,
1499                group,
1500            });
1501        }
1502
1503        // If the `workspace` field is set, we're dealing with a workspace source.
1504        if let Some(workspace) = workspace {
1505            if index.is_some() {
1506                return Err(serde::de::Error::custom(
1507                    "cannot specify both `workspace` and `index`",
1508                ));
1509            }
1510            if git.is_some() {
1511                return Err(serde::de::Error::custom(
1512                    "cannot specify both `workspace` and `git`",
1513                ));
1514            }
1515            if url.is_some() {
1516                return Err(serde::de::Error::custom(
1517                    "cannot specify both `workspace` and `url`",
1518                ));
1519            }
1520            if path.is_some() {
1521                return Err(serde::de::Error::custom(
1522                    "cannot specify both `workspace` and `path`",
1523                ));
1524            }
1525            if rev.is_some() {
1526                return Err(serde::de::Error::custom(
1527                    "cannot specify both `workspace` and `rev`",
1528                ));
1529            }
1530            if tag.is_some() {
1531                return Err(serde::de::Error::custom(
1532                    "cannot specify both `workspace` and `tag`",
1533                ));
1534            }
1535            if branch.is_some() {
1536                return Err(serde::de::Error::custom(
1537                    "cannot specify both `workspace` and `branch`",
1538                ));
1539            }
1540            if package.is_some() {
1541                return Err(serde::de::Error::custom(
1542                    "cannot specify both `workspace` and `package`",
1543                ));
1544            }
1545
1546            return Ok(Self::Workspace {
1547                workspace,
1548                editable,
1549                marker,
1550                extra,
1551                group,
1552            });
1553        }
1554
1555        // If none of the fields are set, we're dealing with an error.
1556        Err(serde::de::Error::custom(
1557            "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1558        ))
1559    }
1560}
1561
1562#[derive(Error, Debug)]
1563pub enum SourceError {
1564    #[error("Failed to resolve Git reference: `{0}`")]
1565    UnresolvedReference(String),
1566    #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1567    WorkspacePackageGit(String),
1568    #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1569    WorkspacePackageUrl(String),
1570    #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1571    WorkspacePackageFile(String),
1572    #[error(
1573        "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1574    )]
1575    UnusedRev(String, String),
1576    #[error(
1577        "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1578    )]
1579    UnusedTag(String, String),
1580    #[error(
1581        "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1582    )]
1583    UnusedBranch(String, String),
1584    #[error(
1585        "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1586    )]
1587    UnusedLfs(String),
1588    #[error(
1589        "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1590    )]
1591    UnusedEditable(String),
1592    #[error("Failed to resolve absolute path")]
1593    Absolute(#[from] std::io::Error),
1594    #[error("Path contains invalid characters: `{}`", _0.display())]
1595    NonUtf8Path(PathBuf),
1596    #[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()
1597    )]
1598    OverlappingMarkers(String, String, String),
1599    #[error(
1600        "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1601    )]
1602    MissingMarkers,
1603    #[error("Must provide at least one source")]
1604    EmptySources,
1605}
1606
1607impl Source {
1608    pub fn from_requirement(
1609        name: &PackageName,
1610        source: RequirementSource,
1611        workspace: bool,
1612        editable: Option<bool>,
1613        index: Option<IndexName>,
1614        rev: Option<String>,
1615        tag: Option<String>,
1616        branch: Option<String>,
1617        lfs: GitLfsSetting,
1618        root: &Path,
1619        existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1620    ) -> Result<Option<Self>, SourceError> {
1621        // If the user specified a Git reference for a non-Git source, try existing Git sources before erroring.
1622        if !matches!(source, RequirementSource::Git { .. })
1623            && (branch.is_some()
1624                || tag.is_some()
1625                || rev.is_some()
1626                || matches!(lfs, GitLfsSetting::Enabled { .. }))
1627        {
1628            if let Some(sources) = existing_sources {
1629                if let Some(package_sources) = sources.get(name) {
1630                    for existing_source in package_sources.iter() {
1631                        if let Self::Git {
1632                            git,
1633                            subdirectory,
1634                            marker,
1635                            extra,
1636                            group,
1637                            ..
1638                        } = existing_source
1639                        {
1640                            return Ok(Some(Self::Git {
1641                                git: git.clone(),
1642                                subdirectory: subdirectory.clone(),
1643                                rev,
1644                                tag,
1645                                branch,
1646                                lfs: lfs.into(),
1647                                marker: *marker,
1648                                extra: extra.clone(),
1649                                group: group.clone(),
1650                            }));
1651                        }
1652                    }
1653                }
1654            }
1655            if let Some(rev) = rev {
1656                return Err(SourceError::UnusedRev(name.to_string(), rev));
1657            }
1658            if let Some(tag) = tag {
1659                return Err(SourceError::UnusedTag(name.to_string(), tag));
1660            }
1661            if let Some(branch) = branch {
1662                return Err(SourceError::UnusedBranch(name.to_string(), branch));
1663            }
1664            if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1665                return Err(SourceError::UnusedLfs(name.to_string()));
1666            }
1667        }
1668
1669        // If we resolved a non-path source, and user specified an `--editable` flag, error.
1670        if !workspace {
1671            if !matches!(source, RequirementSource::Directory { .. }) {
1672                if editable == Some(true) {
1673                    return Err(SourceError::UnusedEditable(name.to_string()));
1674                }
1675            }
1676        }
1677
1678        // If the source is a workspace package, error if the user tried to specify a source.
1679        if workspace {
1680            return match source {
1681                RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1682                    Ok(Some(Self::Workspace {
1683                        workspace: true,
1684                        editable,
1685                        marker: MarkerTree::TRUE,
1686                        extra: None,
1687                        group: None,
1688                    }))
1689                }
1690                RequirementSource::Url { .. } => {
1691                    Err(SourceError::WorkspacePackageUrl(name.to_string()))
1692                }
1693                RequirementSource::Git { .. } => {
1694                    Err(SourceError::WorkspacePackageGit(name.to_string()))
1695                }
1696                RequirementSource::Path { .. } => {
1697                    Err(SourceError::WorkspacePackageFile(name.to_string()))
1698                }
1699            };
1700        }
1701
1702        let source = match source {
1703            RequirementSource::Registry { index: Some(_), .. } => {
1704                return Ok(None);
1705            }
1706            RequirementSource::Registry { index: None, .. } => {
1707                if let Some(index) = index {
1708                    Self::Registry {
1709                        index,
1710                        marker: MarkerTree::TRUE,
1711                        extra: None,
1712                        group: None,
1713                    }
1714                } else {
1715                    return Ok(None);
1716                }
1717            }
1718            RequirementSource::Path { install_path, .. } => Self::Path {
1719                editable: None,
1720                package: None,
1721                path: PortablePathBuf::from(
1722                    relative_to(&install_path, root)
1723                        .or_else(|_| std::path::absolute(&install_path))
1724                        .map_err(SourceError::Absolute)?
1725                        .into_boxed_path(),
1726                ),
1727                marker: MarkerTree::TRUE,
1728                extra: None,
1729                group: None,
1730            },
1731            RequirementSource::Directory {
1732                install_path,
1733                editable: is_editable,
1734                ..
1735            } => Self::Path {
1736                editable: editable.or(is_editable),
1737                package: None,
1738                path: PortablePathBuf::from(
1739                    relative_to(&install_path, root)
1740                        .or_else(|_| std::path::absolute(&install_path))
1741                        .map_err(SourceError::Absolute)?
1742                        .into_boxed_path(),
1743                ),
1744                marker: MarkerTree::TRUE,
1745                extra: None,
1746                group: None,
1747            },
1748            RequirementSource::Url {
1749                location,
1750                subdirectory,
1751                ..
1752            } => Self::Url {
1753                url: location,
1754                subdirectory: subdirectory.map(PortablePathBuf::from),
1755                marker: MarkerTree::TRUE,
1756                extra: None,
1757                group: None,
1758            },
1759            RequirementSource::Git {
1760                git, subdirectory, ..
1761            } => {
1762                if rev.is_none() && tag.is_none() && branch.is_none() {
1763                    let rev = match git.reference() {
1764                        GitReference::Branch(rev) => Some(rev),
1765                        GitReference::Tag(rev) => Some(rev),
1766                        GitReference::BranchOrTag(rev) => Some(rev),
1767                        GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1768                        GitReference::NamedRef(rev) => Some(rev),
1769                        GitReference::DefaultBranch => None,
1770                    };
1771                    Self::Git {
1772                        rev: rev.cloned(),
1773                        tag,
1774                        branch,
1775                        lfs: lfs.into(),
1776                        git: git.repository().clone(),
1777                        subdirectory: subdirectory.map(PortablePathBuf::from),
1778                        marker: MarkerTree::TRUE,
1779                        extra: None,
1780                        group: None,
1781                    }
1782                } else {
1783                    Self::Git {
1784                        rev,
1785                        tag,
1786                        branch,
1787                        lfs: lfs.into(),
1788                        git: git.repository().clone(),
1789                        subdirectory: subdirectory.map(PortablePathBuf::from),
1790                        marker: MarkerTree::TRUE,
1791                        extra: None,
1792                        group: None,
1793                    }
1794                }
1795            }
1796        };
1797
1798        Ok(Some(source))
1799    }
1800
1801    /// Return the [`MarkerTree`] for the source.
1802    pub fn marker(&self) -> MarkerTree {
1803        match self {
1804            Self::Git { marker, .. } => *marker,
1805            Self::Url { marker, .. } => *marker,
1806            Self::Path { marker, .. } => *marker,
1807            Self::Registry { marker, .. } => *marker,
1808            Self::Workspace { marker, .. } => *marker,
1809        }
1810    }
1811
1812    /// Return the extra name for the source.
1813    pub fn extra(&self) -> Option<&ExtraName> {
1814        match self {
1815            Self::Git { extra, .. } => extra.as_ref(),
1816            Self::Url { extra, .. } => extra.as_ref(),
1817            Self::Path { extra, .. } => extra.as_ref(),
1818            Self::Registry { extra, .. } => extra.as_ref(),
1819            Self::Workspace { extra, .. } => extra.as_ref(),
1820        }
1821    }
1822
1823    /// Return the dependency group name for the source.
1824    pub fn group(&self) -> Option<&GroupName> {
1825        match self {
1826            Self::Git { group, .. } => group.as_ref(),
1827            Self::Url { group, .. } => group.as_ref(),
1828            Self::Path { group, .. } => group.as_ref(),
1829            Self::Registry { group, .. } => group.as_ref(),
1830            Self::Workspace { group, .. } => group.as_ref(),
1831        }
1832    }
1833}
1834
1835/// The type of a dependency in a `pyproject.toml`.
1836#[derive(Debug, Clone, PartialEq, Eq)]
1837pub enum DependencyType {
1838    /// A dependency in `project.dependencies`.
1839    Production,
1840    /// A dependency in `tool.uv.dev-dependencies`.
1841    Dev,
1842    /// A dependency in `project.optional-dependencies.{0}`.
1843    Optional(ExtraName),
1844    /// A dependency in `dependency-groups.{0}`.
1845    Group(GroupName),
1846}
1847
1848#[derive(Debug, Clone, PartialEq, Eq)]
1849#[cfg_attr(test, derive(Serialize))]
1850pub struct BuildBackendSettingsSchema;
1851
1852impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1853    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1854    where
1855        D: Deserializer<'de>,
1856    {
1857        Ok(Self)
1858    }
1859}
1860
1861#[cfg(feature = "schemars")]
1862impl schemars::JsonSchema for BuildBackendSettingsSchema {
1863    fn schema_name() -> Cow<'static, str> {
1864        BuildBackendSettings::schema_name()
1865    }
1866
1867    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1868        BuildBackendSettings::json_schema(generator)
1869    }
1870}
1871
1872impl OptionsMetadata for BuildBackendSettingsSchema {
1873    fn record(visit: &mut dyn Visit) {
1874        BuildBackendSettings::record(visit);
1875    }
1876
1877    fn documentation() -> Option<&'static str> {
1878        BuildBackendSettings::documentation()
1879    }
1880
1881    fn metadata() -> OptionSet
1882    where
1883        Self: Sized + 'static,
1884    {
1885        BuildBackendSettings::metadata()
1886    }
1887}