Skip to main content

uv_workspace/
pyproject.rs

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