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