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        /// `true` selects the current workspace. A string selects another workspace discovered
1227        /// from the given path.
1228        ///
1229        /// When set to `false`, the package will be fetched from the remote index, rather than
1230        /// included as a workspace package.
1231        workspace: WorkspaceReference,
1232        /// Whether the package should be installed as editable. Defaults to `true`.
1233        editable: Option<bool>,
1234        #[serde(
1235            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1236            serialize_with = "uv_pep508::marker::ser::serialize",
1237            default
1238        )]
1239        marker: MarkerTree,
1240        extra: Option<ExtraName>,
1241        group: Option<GroupName>,
1242    },
1243}
1244
1245/// A reference to either the current workspace or a workspace discovered from a path.
1246#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1247#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1248#[serde(untagged)]
1249pub enum WorkspaceReference {
1250    Bool(bool),
1251    Path(PortablePathBuf),
1252}
1253
1254/// A custom deserialization implementation for [`Source`]. This is roughly equivalent to
1255/// `#[serde(untagged)]`, but provides more detailed error messages.
1256impl<'de> Deserialize<'de> for Source {
1257    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1258    where
1259        D: Deserializer<'de>,
1260    {
1261        #[derive(Deserialize, Debug, Clone)]
1262        #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1263        struct CatchAll {
1264            git: Option<DisplaySafeUrl>,
1265            subdirectory: Option<PortablePathBuf>,
1266            rev: Option<String>,
1267            tag: Option<String>,
1268            branch: Option<String>,
1269            lfs: Option<bool>,
1270            url: Option<DisplaySafeUrl>,
1271            path: Option<PortablePathBuf>,
1272            editable: Option<bool>,
1273            package: Option<bool>,
1274            index: Option<IndexName>,
1275            workspace: Option<WorkspaceReference>,
1276            #[serde(
1277                skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1278                serialize_with = "uv_pep508::marker::ser::serialize",
1279                default
1280            )]
1281            marker: MarkerTree,
1282            extra: Option<ExtraName>,
1283            group: Option<GroupName>,
1284        }
1285
1286        // Attempt to deserialize as `CatchAll`.
1287        let CatchAll {
1288            git,
1289            subdirectory,
1290            rev,
1291            tag,
1292            branch,
1293            lfs,
1294            url,
1295            path,
1296            editable,
1297            package,
1298            index,
1299            workspace,
1300            marker,
1301            extra,
1302            group,
1303        } = CatchAll::deserialize(deserializer)?;
1304
1305        // If both `extra` and `group` are set, return an error.
1306        if extra.is_some() && group.is_some() {
1307            return Err(serde::de::Error::custom(
1308                "cannot specify both `extra` and `group`",
1309            ));
1310        }
1311
1312        // If the `git` field is set, we're dealing with a Git source.
1313        if let Some(git) = git {
1314            if index.is_some() {
1315                return Err(serde::de::Error::custom(
1316                    "cannot specify both `git` and `index`",
1317                ));
1318            }
1319            if workspace.is_some() {
1320                return Err(serde::de::Error::custom(
1321                    "cannot specify both `git` and `workspace`",
1322                ));
1323            }
1324            if url.is_some() {
1325                return Err(serde::de::Error::custom(
1326                    "cannot specify both `git` and `url`",
1327                ));
1328            }
1329            if editable.is_some() {
1330                return Err(serde::de::Error::custom(
1331                    "cannot specify both `git` and `editable`",
1332                ));
1333            }
1334            if package.is_some() {
1335                return Err(serde::de::Error::custom(
1336                    "cannot specify both `git` and `package`",
1337                ));
1338            }
1339            if subdirectory.is_some() && path.is_some() {
1340                return Err(serde::de::Error::custom(
1341                    "cannot specify both `subdirectory` and `path`",
1342                ));
1343            }
1344
1345            // At most one of `rev`, `tag`, or `branch` may be set.
1346            match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1347                (None, None, None) => {}
1348                (Some(_), None, None) => {}
1349                (None, Some(_), None) => {}
1350                (None, None, Some(_)) => {}
1351                _ => {
1352                    return Err(serde::de::Error::custom(
1353                        "expected at most one of `rev`, `tag`, or `branch`",
1354                    ));
1355                }
1356            }
1357
1358            // If the user prefixed the URL with `git+`, strip it.
1359            let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1360                DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1361            } else {
1362                git
1363            };
1364
1365            return Ok(Self::Git {
1366                git,
1367                subdirectory,
1368                path,
1369                rev,
1370                tag,
1371                branch,
1372                lfs,
1373                marker,
1374                extra,
1375                group,
1376            });
1377        }
1378
1379        // If the `url` field is set, we're dealing with a URL source.
1380        if let Some(url) = url {
1381            if index.is_some() {
1382                return Err(serde::de::Error::custom(
1383                    "cannot specify both `url` and `index`",
1384                ));
1385            }
1386            if workspace.is_some() {
1387                return Err(serde::de::Error::custom(
1388                    "cannot specify both `url` and `workspace`",
1389                ));
1390            }
1391            if path.is_some() {
1392                return Err(serde::de::Error::custom(
1393                    "cannot specify both `url` and `path`",
1394                ));
1395            }
1396            if git.is_some() {
1397                return Err(serde::de::Error::custom(
1398                    "cannot specify both `url` and `git`",
1399                ));
1400            }
1401            if rev.is_some() {
1402                return Err(serde::de::Error::custom(
1403                    "cannot specify both `url` and `rev`",
1404                ));
1405            }
1406            if tag.is_some() {
1407                return Err(serde::de::Error::custom(
1408                    "cannot specify both `url` and `tag`",
1409                ));
1410            }
1411            if branch.is_some() {
1412                return Err(serde::de::Error::custom(
1413                    "cannot specify both `url` and `branch`",
1414                ));
1415            }
1416            if editable.is_some() {
1417                return Err(serde::de::Error::custom(
1418                    "cannot specify both `url` and `editable`",
1419                ));
1420            }
1421            if package.is_some() {
1422                return Err(serde::de::Error::custom(
1423                    "cannot specify both `url` and `package`",
1424                ));
1425            }
1426
1427            return Ok(Self::Url {
1428                url,
1429                subdirectory,
1430                marker,
1431                extra,
1432                group,
1433            });
1434        }
1435
1436        // If the `path` field is set, we're dealing with a path source.
1437        if let Some(path) = path {
1438            if index.is_some() {
1439                return Err(serde::de::Error::custom(
1440                    "cannot specify both `path` and `index`",
1441                ));
1442            }
1443            if workspace.is_some() {
1444                return Err(serde::de::Error::custom(
1445                    "cannot specify both `path` and `workspace`",
1446                ));
1447            }
1448            if git.is_some() {
1449                return Err(serde::de::Error::custom(
1450                    "cannot specify both `path` and `git`",
1451                ));
1452            }
1453            if url.is_some() {
1454                return Err(serde::de::Error::custom(
1455                    "cannot specify both `path` and `url`",
1456                ));
1457            }
1458            if rev.is_some() {
1459                return Err(serde::de::Error::custom(
1460                    "cannot specify both `path` and `rev`",
1461                ));
1462            }
1463            if tag.is_some() {
1464                return Err(serde::de::Error::custom(
1465                    "cannot specify both `path` and `tag`",
1466                ));
1467            }
1468            if branch.is_some() {
1469                return Err(serde::de::Error::custom(
1470                    "cannot specify both `path` and `branch`",
1471                ));
1472            }
1473
1474            // A project must be packaged in order to be installed as editable.
1475            if editable == Some(true) && package == Some(false) {
1476                return Err(serde::de::Error::custom(
1477                    "cannot specify both `editable = true` and `package = false`",
1478                ));
1479            }
1480
1481            return Ok(Self::Path {
1482                path,
1483                editable,
1484                package,
1485                marker,
1486                extra,
1487                group,
1488            });
1489        }
1490
1491        // If the `index` field is set, we're dealing with a registry source.
1492        if let Some(index) = index {
1493            if workspace.is_some() {
1494                return Err(serde::de::Error::custom(
1495                    "cannot specify both `index` and `workspace`",
1496                ));
1497            }
1498            if git.is_some() {
1499                return Err(serde::de::Error::custom(
1500                    "cannot specify both `index` and `git`",
1501                ));
1502            }
1503            if url.is_some() {
1504                return Err(serde::de::Error::custom(
1505                    "cannot specify both `index` and `url`",
1506                ));
1507            }
1508            if path.is_some() {
1509                return Err(serde::de::Error::custom(
1510                    "cannot specify both `index` and `path`",
1511                ));
1512            }
1513            if rev.is_some() {
1514                return Err(serde::de::Error::custom(
1515                    "cannot specify both `index` and `rev`",
1516                ));
1517            }
1518            if tag.is_some() {
1519                return Err(serde::de::Error::custom(
1520                    "cannot specify both `index` and `tag`",
1521                ));
1522            }
1523            if branch.is_some() {
1524                return Err(serde::de::Error::custom(
1525                    "cannot specify both `index` and `branch`",
1526                ));
1527            }
1528            if editable.is_some() {
1529                return Err(serde::de::Error::custom(
1530                    "cannot specify both `index` and `editable`",
1531                ));
1532            }
1533            if package.is_some() {
1534                return Err(serde::de::Error::custom(
1535                    "cannot specify both `index` and `package`",
1536                ));
1537            }
1538
1539            return Ok(Self::Registry {
1540                index,
1541                marker,
1542                extra,
1543                group,
1544            });
1545        }
1546
1547        // If the `workspace` field is set, we're dealing with a workspace source.
1548        if let Some(workspace) = workspace {
1549            if index.is_some() {
1550                return Err(serde::de::Error::custom(
1551                    "cannot specify both `workspace` and `index`",
1552                ));
1553            }
1554            if git.is_some() {
1555                return Err(serde::de::Error::custom(
1556                    "cannot specify both `workspace` and `git`",
1557                ));
1558            }
1559            if url.is_some() {
1560                return Err(serde::de::Error::custom(
1561                    "cannot specify both `workspace` and `url`",
1562                ));
1563            }
1564            if path.is_some() {
1565                return Err(serde::de::Error::custom(
1566                    "cannot specify both `workspace` and `path`",
1567                ));
1568            }
1569            if rev.is_some() {
1570                return Err(serde::de::Error::custom(
1571                    "cannot specify both `workspace` and `rev`",
1572                ));
1573            }
1574            if tag.is_some() {
1575                return Err(serde::de::Error::custom(
1576                    "cannot specify both `workspace` and `tag`",
1577                ));
1578            }
1579            if branch.is_some() {
1580                return Err(serde::de::Error::custom(
1581                    "cannot specify both `workspace` and `branch`",
1582                ));
1583            }
1584            if package.is_some() {
1585                return Err(serde::de::Error::custom(
1586                    "cannot specify both `workspace` and `package`",
1587                ));
1588            }
1589
1590            return Ok(Self::Workspace {
1591                workspace,
1592                editable,
1593                marker,
1594                extra,
1595                group,
1596            });
1597        }
1598
1599        // If none of the fields are set, we're dealing with an error.
1600        Err(serde::de::Error::custom(
1601            "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1602        ))
1603    }
1604}
1605
1606#[derive(Error, Debug)]
1607pub enum SourceError {
1608    #[error("Failed to resolve Git reference: `{0}`")]
1609    UnresolvedReference(String),
1610    #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1611    WorkspacePackageGit(String),
1612    #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1613    WorkspacePackageUrl(String),
1614    #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1615    WorkspacePackageFile(String),
1616    #[error(
1617        "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1618    )]
1619    UnusedRev(String, String),
1620    #[error(
1621        "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1622    )]
1623    UnusedTag(String, String),
1624    #[error(
1625        "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1626    )]
1627    UnusedBranch(String, String),
1628    #[error(
1629        "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1630    )]
1631    UnusedLfs(String),
1632    #[error(
1633        "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1634    )]
1635    UnusedEditable(String),
1636    #[error("Failed to resolve absolute path")]
1637    Absolute(#[from] std::io::Error),
1638    #[error("Path contains invalid characters: `{}`", _0.display())]
1639    NonUtf8Path(PathBuf),
1640    #[error("Source markers must be disjoint, but the following markers overlap: `{0}` and `{1}`.")]
1641    OverlappingMarkers(String, String, String),
1642    #[error(
1643        "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1644    )]
1645    MissingMarkers,
1646    #[error("Must provide at least one source")]
1647    EmptySources,
1648}
1649
1650impl uv_errors::Hint for SourceError {
1651    fn hints(&self) -> uv_errors::Hints<'_> {
1652        match self {
1653            Self::OverlappingMarkers(_, rhs, replacement) => {
1654                uv_errors::Hints::from(format!("replace `{rhs}` with `{replacement}`"))
1655            }
1656            _ => uv_errors::Hints::none(),
1657        }
1658    }
1659}
1660
1661impl Source {
1662    pub fn from_requirement(
1663        name: &PackageName,
1664        source: RequirementSource,
1665        workspace: bool,
1666        editable: Option<bool>,
1667        index: Option<IndexName>,
1668        rev: Option<String>,
1669        tag: Option<String>,
1670        branch: Option<String>,
1671        lfs: GitLfsSetting,
1672        root: &Path,
1673        existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1674    ) -> Result<Option<Self>, SourceError> {
1675        // If the user specified a Git reference for a non-Git source, try existing Git sources before erroring.
1676        if !matches!(
1677            source,
1678            RequirementSource::GitDirectory { .. } | RequirementSource::GitPath { .. }
1679        ) && (branch.is_some()
1680            || tag.is_some()
1681            || rev.is_some()
1682            || matches!(lfs, GitLfsSetting::Enabled { .. }))
1683        {
1684            if let Some(sources) = existing_sources
1685                && let Some(package_sources) = sources.get(name)
1686            {
1687                for existing_source in package_sources.iter() {
1688                    if let Self::Git {
1689                        git,
1690                        subdirectory,
1691                        path,
1692                        marker,
1693                        extra,
1694                        group,
1695                        ..
1696                    } = existing_source
1697                    {
1698                        return Ok(Some(Self::Git {
1699                            git: git.clone(),
1700                            subdirectory: subdirectory.clone(),
1701                            rev,
1702                            tag,
1703                            branch,
1704                            lfs: lfs.into(),
1705                            marker: *marker,
1706                            path: path.clone(),
1707                            extra: extra.clone(),
1708                            group: group.clone(),
1709                        }));
1710                    }
1711                }
1712            }
1713            if let Some(rev) = rev {
1714                return Err(SourceError::UnusedRev(name.to_string(), rev));
1715            }
1716            if let Some(tag) = tag {
1717                return Err(SourceError::UnusedTag(name.to_string(), tag));
1718            }
1719            if let Some(branch) = branch {
1720                return Err(SourceError::UnusedBranch(name.to_string(), branch));
1721            }
1722            if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1723                return Err(SourceError::UnusedLfs(name.to_string()));
1724            }
1725        }
1726
1727        // If we resolved a non-path source, and user specified an `--editable` flag, error.
1728        if !workspace {
1729            if !matches!(source, RequirementSource::Directory { .. }) {
1730                if editable == Some(true) {
1731                    return Err(SourceError::UnusedEditable(name.to_string()));
1732                }
1733            }
1734        }
1735
1736        // If the source is a workspace package, error if the user tried to specify a source.
1737        if workspace {
1738            return match source {
1739                RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1740                    Ok(Some(Self::Workspace {
1741                        workspace: WorkspaceReference::Bool(true),
1742                        editable,
1743                        marker: MarkerTree::TRUE,
1744                        extra: None,
1745                        group: None,
1746                    }))
1747                }
1748                RequirementSource::Url { .. } => {
1749                    Err(SourceError::WorkspacePackageUrl(name.to_string()))
1750                }
1751                RequirementSource::GitDirectory { .. } => {
1752                    Err(SourceError::WorkspacePackageGit(name.to_string()))
1753                }
1754                RequirementSource::GitPath { .. } => {
1755                    Err(SourceError::WorkspacePackageGit(name.to_string()))
1756                }
1757                RequirementSource::Path { .. } => {
1758                    Err(SourceError::WorkspacePackageFile(name.to_string()))
1759                }
1760            };
1761        }
1762
1763        let source = match source {
1764            RequirementSource::Registry { index: Some(_), .. } => {
1765                return Ok(None);
1766            }
1767            RequirementSource::Registry { index: None, .. } if let Some(index) = index => {
1768                Self::Registry {
1769                    index,
1770                    marker: MarkerTree::TRUE,
1771                    extra: None,
1772                    group: None,
1773                }
1774            }
1775            RequirementSource::Registry { index: None, .. } => return Ok(None),
1776            RequirementSource::Path { install_path, .. } => Self::Path {
1777                editable: None,
1778                package: None,
1779                path: PortablePathBuf::from(
1780                    relative_to(&install_path, root)
1781                        .or_else(|_| std::path::absolute(&install_path))
1782                        .map_err(SourceError::Absolute)?
1783                        .into_boxed_path(),
1784                ),
1785                marker: MarkerTree::TRUE,
1786                extra: None,
1787                group: None,
1788            },
1789            RequirementSource::Directory {
1790                install_path,
1791                editable: is_editable,
1792                ..
1793            } => Self::Path {
1794                editable: editable.or(is_editable),
1795                package: None,
1796                path: PortablePathBuf::from(
1797                    relative_to(&install_path, root)
1798                        .or_else(|_| std::path::absolute(&install_path))
1799                        .map_err(SourceError::Absolute)?
1800                        .into_boxed_path(),
1801                ),
1802                marker: MarkerTree::TRUE,
1803                extra: None,
1804                group: None,
1805            },
1806            RequirementSource::Url {
1807                location,
1808                subdirectory,
1809                ..
1810            } => Self::Url {
1811                url: location,
1812                subdirectory: subdirectory.map(PortablePathBuf::from),
1813                marker: MarkerTree::TRUE,
1814                extra: None,
1815                group: None,
1816            },
1817            RequirementSource::GitDirectory {
1818                git, subdirectory, ..
1819            } => {
1820                if rev.is_none() && tag.is_none() && branch.is_none() {
1821                    let rev = match git.reference() {
1822                        GitReference::Branch(rev) => Some(rev),
1823                        GitReference::Tag(rev) => Some(rev),
1824                        GitReference::BranchOrTag(rev) => Some(rev),
1825                        GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1826                        GitReference::NamedRef(rev) => Some(rev),
1827                        GitReference::DefaultBranch => None,
1828                    };
1829                    Self::Git {
1830                        rev: rev.cloned(),
1831                        tag,
1832                        branch,
1833                        lfs: lfs.into(),
1834                        git: git.url().clone(),
1835                        subdirectory: subdirectory.map(PortablePathBuf::from),
1836                        path: None,
1837                        marker: MarkerTree::TRUE,
1838                        extra: None,
1839                        group: None,
1840                    }
1841                } else {
1842                    Self::Git {
1843                        rev,
1844                        tag,
1845                        branch,
1846                        lfs: lfs.into(),
1847                        git: git.url().clone(),
1848                        subdirectory: subdirectory.map(PortablePathBuf::from),
1849                        path: None,
1850                        marker: MarkerTree::TRUE,
1851                        extra: None,
1852                        group: None,
1853                    }
1854                }
1855            }
1856            RequirementSource::GitPath {
1857                git, install_path, ..
1858            } => {
1859                if rev.is_none() && tag.is_none() && branch.is_none() {
1860                    let rev = match git.reference() {
1861                        GitReference::Branch(rev) => Some(rev),
1862                        GitReference::Tag(rev) => Some(rev),
1863                        GitReference::BranchOrTag(rev) => Some(rev),
1864                        GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1865                        GitReference::NamedRef(rev) => Some(rev),
1866                        GitReference::DefaultBranch => None,
1867                    };
1868                    Self::Git {
1869                        rev: rev.cloned(),
1870                        tag,
1871                        branch,
1872                        lfs: lfs.into(),
1873                        git: git.url().clone(),
1874                        subdirectory: None,
1875                        path: Some(PortablePathBuf::from(install_path.as_path())),
1876                        marker: MarkerTree::TRUE,
1877                        extra: None,
1878                        group: None,
1879                    }
1880                } else {
1881                    Self::Git {
1882                        rev,
1883                        tag,
1884                        branch,
1885                        lfs: lfs.into(),
1886                        git: git.url().clone(),
1887                        subdirectory: None,
1888                        path: Some(PortablePathBuf::from(install_path.as_path())),
1889                        marker: MarkerTree::TRUE,
1890                        extra: None,
1891                        group: None,
1892                    }
1893                }
1894            }
1895        };
1896
1897        Ok(Some(source))
1898    }
1899
1900    /// Return the [`MarkerTree`] for the source.
1901    pub fn marker(&self) -> MarkerTree {
1902        match self {
1903            Self::Git { marker, .. } => *marker,
1904            Self::Url { marker, .. } => *marker,
1905            Self::Path { marker, .. } => *marker,
1906            Self::Registry { marker, .. } => *marker,
1907            Self::Workspace { marker, .. } => *marker,
1908        }
1909    }
1910
1911    /// Return the extra name for the source.
1912    pub fn extra(&self) -> Option<&ExtraName> {
1913        match self {
1914            Self::Git { extra, .. } => extra.as_ref(),
1915            Self::Url { extra, .. } => extra.as_ref(),
1916            Self::Path { extra, .. } => extra.as_ref(),
1917            Self::Registry { extra, .. } => extra.as_ref(),
1918            Self::Workspace { extra, .. } => extra.as_ref(),
1919        }
1920    }
1921
1922    /// Return the dependency group name for the source.
1923    pub fn group(&self) -> Option<&GroupName> {
1924        match self {
1925            Self::Git { group, .. } => group.as_ref(),
1926            Self::Url { group, .. } => group.as_ref(),
1927            Self::Path { group, .. } => group.as_ref(),
1928            Self::Registry { group, .. } => group.as_ref(),
1929            Self::Workspace { group, .. } => group.as_ref(),
1930        }
1931    }
1932}
1933
1934/// The type of a dependency in a `pyproject.toml`.
1935#[derive(Debug, Clone, PartialEq, Eq)]
1936pub enum DependencyType {
1937    /// A dependency in `project.dependencies`.
1938    Production,
1939    /// A dependency in `tool.uv.dev-dependencies`.
1940    Dev,
1941    /// A dependency in `project.optional-dependencies.{0}`.
1942    Optional(ExtraName),
1943    /// A dependency in `dependency-groups.{0}`.
1944    Group(GroupName),
1945}
1946
1947impl DependencyType {
1948    /// Return the TOML table name(s) for this dependency type.
1949    pub fn toml_table_name(&self) -> Cow<'_, str> {
1950        match self {
1951            Self::Production => Cow::Borrowed("`project.dependencies`"),
1952            Self::Dev => {
1953                Cow::Borrowed("`tool.uv.dev-dependencies` or `tool.uv.dependency-groups.dev`")
1954            }
1955            Self::Optional(extra) => Cow::Owned(format!("`project.optional-dependencies.{extra}`")),
1956            Self::Group(group) => Cow::Owned(format!("`dependency-groups.{group}`")),
1957        }
1958    }
1959}
1960
1961#[derive(Debug, Clone, PartialEq, Eq)]
1962#[cfg_attr(test, derive(Serialize))]
1963pub(crate) struct BuildBackendSettingsSchema;
1964
1965impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1966    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1967    where
1968        D: Deserializer<'de>,
1969    {
1970        Ok(Self)
1971    }
1972}
1973
1974#[cfg(feature = "schemars")]
1975impl schemars::JsonSchema for BuildBackendSettingsSchema {
1976    fn schema_name() -> Cow<'static, str> {
1977        BuildBackendSettings::schema_name()
1978    }
1979
1980    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1981        BuildBackendSettings::json_schema(generator)
1982    }
1983}
1984
1985impl OptionsMetadata for BuildBackendSettingsSchema {
1986    fn record(visit: &mut dyn Visit) {
1987        BuildBackendSettings::record(visit);
1988    }
1989
1990    fn documentation() -> Option<&'static str> {
1991        BuildBackendSettings::documentation()
1992    }
1993
1994    fn metadata() -> OptionSet
1995    where
1996        Self: Sized + 'static,
1997    {
1998        BuildBackendSettings::metadata()
1999    }
2000}