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#[derive(Debug, Clone, Deserialize)]
15#[serde(try_from = "ToolWire", into = "ToolWire")]
16pub struct Tool {
17 requirements: Vec<Requirement>,
19 constraints: Vec<Requirement>,
21 overrides: Vec<Requirement>,
23 build_constraints: Vec<Requirement>,
25 python: Option<PythonRequest>,
27 entrypoints: Vec<ToolEntrypoint>,
29 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 Requirement(Requirement),
55 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
135fn 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 value.decor_mut().set_prefix("\n ");
151 value
152 })
153 .collect::<Array>();
154 array.set_trailing_comma(true);
157 array.set_trailing("\n");
159 array
160}
161
162impl Tool {
163 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 #[must_use]
188 pub fn with_options(self, options: ToolOptions) -> Self {
189 Self { options, ..self }
190 }
191
192 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 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 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 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}