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