Skip to main content

uv_workspace/
pyproject.rs

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