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