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#[derive(Debug, Clone, Deserialize)]
16#[serde(try_from = "ToolWire", into = "ToolWire")]
17pub struct Tool {
18 requirements: Vec<Requirement>,
23 constraints: Vec<Requirement>,
25 overrides: Vec<Requirement>,
27 excludes: Vec<PackageName>,
29 build_constraints: Vec<Requirement>,
31 python: Option<PythonRequest>,
33 entrypoints: Vec<ToolEntrypoint>,
35 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 Requirement(Requirement),
63 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
145fn 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 value.decor_mut().set_prefix("\n ");
161 value
162 })
163 .collect::<Array>();
164 array.set_trailing_comma(true);
167 array.set_trailing("\n");
169 array
170}
171
172impl Tool {
173 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 #[must_use]
200 pub fn with_options(self, options: ToolOptions) -> Self {
201 Self { options, ..self }
202 }
203
204 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 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 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 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 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}