Skip to main content

uv_tool/
tool.rs

1use std::fmt::{self, Display, Formatter};
2use std::path::PathBuf;
3
4use serde::Deserialize;
5use toml_edit::{Array, Item, Table, Value, value};
6
7use uv_distribution_types::Requirement;
8use uv_fs::{PortablePath, Simplified};
9use uv_normalize::PackageName;
10use uv_pypi_types::VerbatimParsedUrl;
11use uv_python::PythonRequest;
12use uv_settings::ToolOptions;
13
14/// A tool entry.
15#[derive(Debug, Clone, Deserialize)]
16#[serde(try_from = "ToolWire", into = "ToolWire")]
17pub struct Tool {
18    /// The requirements requested by the user during installation.
19    requirements: Vec<Requirement>,
20    /// The constraints requested by the user during installation.
21    constraints: Vec<Requirement>,
22    /// The overrides requested by the user during installation.
23    overrides: Vec<Requirement>,
24    /// The excludes requested by the user during installation.
25    excludes: Vec<PackageName>,
26    /// The build constraints requested by the user during installation.
27    build_constraints: Vec<Requirement>,
28    /// The Python requested by the user during installation.
29    python: Option<PythonRequest>,
30    /// A mapping of entry point names to their metadata.
31    entrypoints: Vec<ToolEntrypoint>,
32    /// The [`ToolOptions`] used to install this tool.
33    options: ToolOptions,
34}
35
36#[derive(Debug, Clone, Deserialize)]
37#[serde(rename_all = "kebab-case")]
38struct ToolWire {
39    #[serde(default)]
40    requirements: Vec<RequirementWire>,
41    #[serde(default)]
42    constraints: Vec<Requirement>,
43    #[serde(default)]
44    overrides: Vec<Requirement>,
45    #[serde(default)]
46    excludes: Vec<PackageName>,
47    #[serde(default)]
48    build_constraint_dependencies: Vec<Requirement>,
49    python: Option<PythonRequest>,
50    entrypoints: Vec<ToolEntrypoint>,
51    #[serde(default)]
52    options: ToolOptions,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
56#[serde(untagged)]
57enum RequirementWire {
58    /// A [`Requirement`] following our uv-specific schema.
59    Requirement(Requirement),
60    /// A PEP 508-compatible requirement. We no longer write these, but there might be receipts out
61    /// there that still use them.
62    Deprecated(uv_pep508::Requirement<VerbatimParsedUrl>),
63}
64
65impl From<Tool> for ToolWire {
66    fn from(tool: Tool) -> Self {
67        Self {
68            requirements: tool
69                .requirements
70                .into_iter()
71                .map(RequirementWire::Requirement)
72                .collect(),
73            constraints: tool.constraints,
74            overrides: tool.overrides,
75            excludes: tool.excludes,
76            build_constraint_dependencies: tool.build_constraints,
77            python: tool.python,
78            entrypoints: tool.entrypoints,
79            options: tool.options,
80        }
81    }
82}
83
84impl TryFrom<ToolWire> for Tool {
85    type Error = serde::de::value::Error;
86
87    fn try_from(tool: ToolWire) -> Result<Self, Self::Error> {
88        Ok(Self {
89            requirements: tool
90                .requirements
91                .into_iter()
92                .map(|req| match req {
93                    RequirementWire::Requirement(requirements) => requirements,
94                    RequirementWire::Deprecated(requirement) => Requirement::from(requirement),
95                })
96                .collect(),
97            constraints: tool.constraints,
98            overrides: tool.overrides,
99            excludes: tool.excludes,
100            build_constraints: tool.build_constraint_dependencies,
101            python: tool.python,
102            entrypoints: tool.entrypoints,
103            options: tool.options,
104        })
105    }
106}
107
108#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize)]
109#[serde(rename_all = "kebab-case")]
110pub struct ToolEntrypoint {
111    pub name: String,
112    pub install_path: PathBuf,
113    pub from: Option<String>,
114}
115
116impl Display for ToolEntrypoint {
117    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
118        #[cfg(windows)]
119        {
120            write!(
121                f,
122                "{} ({})",
123                self.name,
124                self.install_path
125                    .simplified_display()
126                    .to_string()
127                    .replace('/', "\\")
128            )
129        }
130        #[cfg(unix)]
131        {
132            write!(
133                f,
134                "{} ({})",
135                self.name,
136                self.install_path.simplified_display()
137            )
138        }
139    }
140}
141
142/// Format an array so that each element is on its own line and has a trailing comma.
143///
144/// Example:
145///
146/// ```toml
147/// requirements = [
148///     "foo",
149///     "bar",
150/// ]
151/// ```
152fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
153    let mut array = elements
154        .map(Into::into)
155        .map(|mut value| {
156            // Each dependency is on its own line and indented.
157            value.decor_mut().set_prefix("\n    ");
158            value
159        })
160        .collect::<Array>();
161    // With a trailing comma, inserting another entry doesn't change the preceding line,
162    // reducing the diff noise.
163    array.set_trailing_comma(true);
164    // The line break between the last element's comma and the closing square bracket.
165    array.set_trailing("\n");
166    array
167}
168
169impl Tool {
170    /// Create a new `Tool`.
171    pub fn new(
172        requirements: Vec<Requirement>,
173        constraints: Vec<Requirement>,
174        overrides: Vec<Requirement>,
175        excludes: Vec<PackageName>,
176        build_constraints: Vec<Requirement>,
177        python: Option<PythonRequest>,
178        entrypoints: impl IntoIterator<Item = ToolEntrypoint>,
179        options: ToolOptions,
180    ) -> Self {
181        let mut entrypoints: Vec<_> = entrypoints.into_iter().collect();
182        entrypoints.sort();
183        Self {
184            requirements,
185            constraints,
186            overrides,
187            excludes,
188            build_constraints,
189            python,
190            entrypoints,
191            options,
192        }
193    }
194
195    /// Create a new [`Tool`] with the given [`ToolOptions`].
196    #[must_use]
197    pub fn with_options(self, options: ToolOptions) -> Self {
198        Self { options, ..self }
199    }
200
201    /// Returns the TOML table for this tool.
202    pub(crate) fn to_toml(&self) -> Result<Table, toml_edit::ser::Error> {
203        let mut table = Table::new();
204
205        if !self.requirements.is_empty() {
206            table.insert("requirements", {
207                let requirements = self
208                    .requirements
209                    .iter()
210                    .map(|requirement| {
211                        serde::Serialize::serialize(
212                            &requirement,
213                            toml_edit::ser::ValueSerializer::new(),
214                        )
215                    })
216                    .collect::<Result<Vec<_>, _>>()?;
217
218                let requirements = match requirements.as_slice() {
219                    [] => Array::new(),
220                    [requirement] => Array::from_iter([requirement]),
221                    requirements => each_element_on_its_line_array(requirements.iter()),
222                };
223                value(requirements)
224            });
225        }
226
227        if !self.constraints.is_empty() {
228            table.insert("constraints", {
229                let constraints = self
230                    .constraints
231                    .iter()
232                    .map(|constraint| {
233                        serde::Serialize::serialize(
234                            &constraint,
235                            toml_edit::ser::ValueSerializer::new(),
236                        )
237                    })
238                    .collect::<Result<Vec<_>, _>>()?;
239
240                let constraints = match constraints.as_slice() {
241                    [] => Array::new(),
242                    [constraint] => Array::from_iter([constraint]),
243                    constraints => each_element_on_its_line_array(constraints.iter()),
244                };
245                value(constraints)
246            });
247        }
248
249        if !self.overrides.is_empty() {
250            table.insert("overrides", {
251                let overrides = self
252                    .overrides
253                    .iter()
254                    .map(|r#override| {
255                        serde::Serialize::serialize(
256                            &r#override,
257                            toml_edit::ser::ValueSerializer::new(),
258                        )
259                    })
260                    .collect::<Result<Vec<_>, _>>()?;
261
262                let overrides = match overrides.as_slice() {
263                    [] => Array::new(),
264                    [r#override] => Array::from_iter([r#override]),
265                    overrides => each_element_on_its_line_array(overrides.iter()),
266                };
267                value(overrides)
268            });
269        }
270
271        if !self.excludes.is_empty() {
272            table.insert("excludes", {
273                let excludes = self
274                    .excludes
275                    .iter()
276                    .map(|r#exclude| {
277                        serde::Serialize::serialize(
278                            &r#exclude,
279                            toml_edit::ser::ValueSerializer::new(),
280                        )
281                    })
282                    .collect::<Result<Vec<_>, _>>()?;
283
284                let excludes = match excludes.as_slice() {
285                    [] => Array::new(),
286                    [r#exclude] => Array::from_iter([r#exclude]),
287                    excludes => each_element_on_its_line_array(excludes.iter()),
288                };
289                value(excludes)
290            });
291        }
292
293        if !self.build_constraints.is_empty() {
294            table.insert("build-constraint-dependencies", {
295                let build_constraints = self
296                    .build_constraints
297                    .iter()
298                    .map(|r#build_constraint| {
299                        serde::Serialize::serialize(
300                            &r#build_constraint,
301                            toml_edit::ser::ValueSerializer::new(),
302                        )
303                    })
304                    .collect::<Result<Vec<_>, _>>()?;
305
306                let build_constraints = match build_constraints.as_slice() {
307                    [] => Array::new(),
308                    [r#build_constraint] => Array::from_iter([r#build_constraint]),
309                    build_constraints => each_element_on_its_line_array(build_constraints.iter()),
310                };
311                value(build_constraints)
312            });
313        }
314
315        if let Some(ref python) = self.python {
316            table.insert(
317                "python",
318                value(serde::Serialize::serialize(
319                    &python,
320                    toml_edit::ser::ValueSerializer::new(),
321                )?),
322            );
323        }
324
325        table.insert("entrypoints", {
326            let entrypoints = each_element_on_its_line_array(
327                self.entrypoints
328                    .iter()
329                    .map(ToolEntrypoint::to_toml)
330                    .map(Table::into_inline_table),
331            );
332            value(entrypoints)
333        });
334
335        if self.options != ToolOptions::default() {
336            let serialized =
337                serde::Serialize::serialize(&self.options, toml_edit::ser::ValueSerializer::new())?;
338            let Value::InlineTable(serialized) = serialized else {
339                return Err(toml_edit::ser::Error::Custom(
340                    "Expected an inline table".to_string(),
341                ));
342            };
343            table.insert("options", Item::Table(serialized.into_table()));
344        }
345
346        Ok(table)
347    }
348
349    pub fn entrypoints(&self) -> &[ToolEntrypoint] {
350        &self.entrypoints
351    }
352
353    pub fn requirements(&self) -> &[Requirement] {
354        &self.requirements
355    }
356
357    pub fn constraints(&self) -> &[Requirement] {
358        &self.constraints
359    }
360
361    pub fn overrides(&self) -> &[Requirement] {
362        &self.overrides
363    }
364
365    pub fn excludes(&self) -> &[PackageName] {
366        &self.excludes
367    }
368
369    pub fn build_constraints(&self) -> &[Requirement] {
370        &self.build_constraints
371    }
372
373    pub fn python(&self) -> &Option<PythonRequest> {
374        &self.python
375    }
376
377    pub fn options(&self) -> &ToolOptions {
378        &self.options
379    }
380}
381
382impl ToolEntrypoint {
383    /// Create a new [`ToolEntrypoint`].
384    pub fn new(name: &str, install_path: PathBuf, from: String) -> Self {
385        let name = name
386            .trim_end_matches(std::env::consts::EXE_SUFFIX)
387            .to_string();
388        Self {
389            name,
390            install_path,
391            from: Some(from),
392        }
393    }
394
395    /// Returns the TOML table for this entrypoint.
396    pub(crate) fn to_toml(&self) -> Table {
397        let mut table = Table::new();
398        table.insert("name", value(&self.name));
399        table.insert(
400            "install-path",
401            // Use cross-platform slashes so the toml string type does not change
402            value(PortablePath::from(&self.install_path).to_string()),
403        );
404        if let Some(from) = &self.from {
405            table.insert("from", value(from));
406        }
407        table
408    }
409}