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#[derive(Debug, Clone, Deserialize)]
16#[serde(try_from = "ToolWire", into = "ToolWire")]
17pub struct Tool {
18 requirements: Vec<Requirement>,
20 constraints: Vec<Requirement>,
22 overrides: Vec<Requirement>,
24 excludes: Vec<PackageName>,
26 build_constraints: Vec<Requirement>,
28 python: Option<PythonRequest>,
30 entrypoints: Vec<ToolEntrypoint>,
32 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 Requirement(Requirement),
60 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
142fn 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 value.decor_mut().set_prefix("\n ");
158 value
159 })
160 .collect::<Array>();
161 array.set_trailing_comma(true);
164 array.set_trailing("\n");
166 array
167}
168
169impl Tool {
170 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 #[must_use]
197 pub fn with_options(self, options: ToolOptions) -> Self {
198 Self { options, ..self }
199 }
200
201 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 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 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 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}