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