Skip to main content

uv_workspace/
pyproject.rs

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