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