1use std::{
2 borrow::Borrow,
3 collections::{BTreeMap, VecDeque},
4 env,
5 ffi::{OsStr, OsString},
6 fmt, fs,
7 path::PathBuf,
8};
9
10use serde::de::{value::Error as DeError, Error as DeErrorT};
11use serde_derive::{Deserialize, Serialize};
12use toml::{self, from_str, to_string};
13
14use crate::recipes::find;
15
16fn is_zero(n: &u64) -> bool {
17 *n == 0
18}
19
20#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, PartialOrd)]
21#[serde(default)]
22pub struct Package {
23 pub name: PackageName,
25 #[serde(skip_serializing_if = "String::is_empty")]
27 pub version: String,
28 pub target: String,
30 #[serde(skip_serializing_if = "String::is_empty")]
32 pub blake3: String,
33 #[serde(skip_serializing_if = "is_zero")]
35 pub storage_size: u64,
36 #[serde(skip_serializing_if = "is_zero")]
38 pub network_size: u64,
39 pub depends: Vec<PackageName>,
41}
42
43impl Package {
44 pub fn new(name: &PackageName) -> Result<Self, PackageError> {
45 let dir = find(name.name()).ok_or_else(|| PackageError::PackageNotFound(name.clone()))?;
46 let target = env::var("TARGET").map_err(|_| PackageError::TargetInvalid)?;
47
48 let file = dir.join("target").join(target).join("stage.toml");
49 if !file.is_file() {
50 return Err(PackageError::FileMissing(file));
51 }
52
53 let toml = fs::read_to_string(&file)
54 .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?;
55 toml::from_str(&toml).map_err(|err| PackageError::Parse(DeError::custom(err), Some(file)))
56 }
57
58 pub fn new_recursive(
59 names: &[PackageName],
60 nonstop: bool,
61 recursion: usize,
62 ) -> Result<Vec<Self>, PackageError> {
63 if recursion == 0 {
64 return Err(PackageError::Recursion(Default::default()));
65 }
66
67 let mut packages = Vec::new();
68 let mut last_err = None;
69 for name in names {
70 let package = match Self::new(name) {
71 Ok(p) => p,
72 Err(e) => {
73 if nonstop {
74 last_err = Some(e);
75 continue;
76 } else {
77 return Err(e);
78 }
79 }
80 };
81
82 let dependencies = match Self::new_recursive(&package.depends, nonstop, recursion - 1) {
83 Ok(p) => p,
84 Err(mut e) => {
85 e.append_recursion(name);
86 if nonstop {
87 last_err = Some(e);
88 continue;
89 } else {
90 return Err(e);
91 }
92 }
93 };
94
95 for dependency in dependencies {
96 if !packages.contains(&dependency) {
97 packages.push(dependency);
98 }
99 }
100
101 if !packages.contains(&package) {
102 packages.push(package);
103 }
104 }
105
106 if packages.len() == 0 && last_err.is_some() {
107 return Err(last_err.unwrap());
108 }
109
110 Ok(packages)
111 }
112
113 pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
114 from_str(text)
115 }
116
117 #[allow(dead_code)]
118 pub fn to_toml(&self) -> String {
119 to_string(self).unwrap()
122 }
123}
124
125#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
132#[serde(into = "String")]
133#[serde(try_from = "String")]
134pub struct PackageName(String);
135
136impl PackageName {
137 pub fn new(name: impl Into<String>) -> Result<Self, PackageError> {
138 let name = name.into();
139 if name.is_empty() {
141 return Err(PackageError::PackageNameInvalid(name));
142 }
143 let mut separators = 0;
144 let mut has_host_prefix = false;
145 for c in name.chars() {
146 if "/\0".contains(c) {
147 return Err(PackageError::PackageNameInvalid(name));
148 }
149 if c == '.' {
150 separators += 1;
151 if separators > 1 {
152 return Err(PackageError::PackageNameInvalid(name));
153 }
154 }
155 if c == ':' {
156 if has_host_prefix {
157 return Err(PackageError::PackageNameInvalid(name));
158 }
159 has_host_prefix = true;
160 }
161 }
162 let r = Self(name);
163 if has_host_prefix && !r.is_host() {
164 return Err(PackageError::PackageNameInvalid(r.0));
165 }
166 Ok(r)
167 }
168
169 pub fn as_str(&self) -> &str {
170 self.0.as_str()
171 }
172
173 pub fn is_host(&self) -> bool {
175 self.0.starts_with("host:")
176 }
177
178 pub fn name(&self) -> &str {
180 let mut s = self.0.as_str();
181 if self.is_host() {
182 s = &s[5..]
183 }
184 if let Some(pos) = s.find('.') {
185 s = &s[..pos]
186 }
187 s
188 }
189
190 pub fn suffix(&self) -> Option<&str> {
192 let mut s = self.0.as_str();
193 if self.is_host() {
194 s = &s[5..]
195 }
196 if let Some(pos) = s.find('.') {
197 Some(&s[pos + 1..])
198 } else {
199 None
200 }
201 }
202
203 pub fn without_host(&self) -> PackageName {
205 let name = if self.is_host() {
206 &self.as_str()["host:".len()..]
207 } else {
208 self.as_str()
209 };
210
211 Self(name.to_string())
212 }
213
214 pub fn with_host(&self) -> PackageName {
216 let name = if self.is_host() {
217 self.as_str().to_string()
218 } else {
219 format!("host:{}", self.as_str())
220 };
221
222 Self(name)
223 }
224
225 pub fn with_suffix(&self, suffix: Option<&str>) -> PackageName {
227 let mut name = self.name().to_string();
228 if let Some(suffix) = suffix {
229 name.push('.');
230 name.push_str(suffix);
231 }
232
233 Self(name)
234 }
235}
236
237impl From<PackageName> for String {
238 fn from(package_name: PackageName) -> Self {
239 package_name.0
240 }
241}
242
243impl TryFrom<String> for PackageName {
244 type Error = PackageError;
245 fn try_from(name: String) -> Result<Self, Self::Error> {
246 Self::new(name)
247 }
248}
249
250impl TryFrom<&str> for PackageName {
251 type Error = PackageError;
252 fn try_from(name: &str) -> Result<Self, Self::Error> {
253 Self::new(name)
254 }
255}
256
257impl TryFrom<&OsStr> for PackageName {
258 type Error = PackageError;
259 fn try_from(name: &OsStr) -> Result<Self, Self::Error> {
260 let name = name
261 .to_str()
262 .ok_or_else(|| PackageError::PackageNameInvalid(name.to_string_lossy().to_string()))?;
263 Self::new(name)
264 }
265}
266
267impl TryFrom<OsString> for PackageName {
268 type Error = PackageError;
269 fn try_from(name: OsString) -> Result<Self, Self::Error> {
270 name.as_os_str().try_into()
271 }
272}
273
274impl fmt::Display for PackageName {
275 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
276 write!(f, "{}", self.0)
277 }
278}
279
280impl Borrow<str> for PackageName {
281 fn borrow(&self) -> &str {
282 self.as_str()
283 }
284}
285
286#[derive(Debug)]
287pub struct PackageInfo {
288 pub installed: bool,
289 pub package: Package,
290}
291
292#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
293pub struct Repository {
294 pub packages: BTreeMap<String, String>,
295}
296
297impl Repository {
298 pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
299 from_str(text)
300 }
301}
302
303#[derive(Debug, thiserror::Error)]
307pub enum PackageError {
308 #[error("Missing package file {0:?}")]
309 FileMissing(PathBuf),
310 #[error("Package {0:?} name invalid")]
311 PackageNameInvalid(String),
312 #[error("Package {0:?} not found")]
313 PackageNotFound(PackageName),
314 #[error("Failed parsing package: {0}; file: {1:?}")]
315 Parse(serde::de::value::Error, Option<PathBuf>),
316 #[error("Recursion limit reached while processing dependencies; tree: {0:?}")]
317 Recursion(VecDeque<PackageName>),
318 #[error("TARGET triplet env var unset or invalid")]
319 TargetInvalid,
320}
321
322impl PackageError {
323 pub fn append_recursion(&mut self, name: &PackageName) {
329 if let PackageError::Recursion(ref mut packages) = self {
330 packages.push_front(name.clone());
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use std::collections::BTreeMap;
338
339 use crate::package::Repository;
340
341 use super::{Package, PackageName};
342
343 const WORKING_DEPENDS: &str = r#"
344 name = "gzdoom"
345 version = "TODO"
346 target = "x86_64-unknown-redox"
347 depends = ["gtk3", "sdl2", "zmusic"]
348 "#;
349
350 const WORKING_NO_DEPENDS: &str = r#"
351 name = "kmquake2"
352 version = "TODO"
353 target = "x86_64-unknown-redox"
354 "#;
355
356 const WORKING_EMPTY_DEPENDS: &str = r#"
357 name = "iodoom3"
358 version = "TODO"
359 target = "x86_64-unknown-redox"
360 depends = []
361 "#;
362
363 const WORKING_EMPTY_VERSION: &str = r#"
364 name = "dev-essentials"
365 target = "x86_64-unknown-redox"
366 depends = ["gcc13"]
367 "#;
368
369 const WORKING_REPOSITORY: &str = r#"
370 [packages]
371 foo = "bar"
372 "#;
373
374 const INVALID_NAME: &str = r#"
375 name = "dolphin.emu.lator"
376 version = "TODO"
377 target = "x86_64-unknown-redox"
378 depends = ["qt5"]
379 "#;
380
381 const INVALID_NAME_DEPENDS: &str = r#"
382 name = "mgba"
383 version = "TODO"
384 target = "x86_64-unknown-redox"
385 depends = ["ffmpeg:latest"]
386 "#;
387
388 #[test]
389 fn package_name_split() -> Result<(), toml::de::Error> {
390 let name1 = PackageName::new("foo").unwrap();
391 let name2 = PackageName::new("foo.bar").unwrap();
392 let name3 = PackageName::new("host:foo").unwrap();
393 let name4 = PackageName::new("host:foo.").unwrap();
394 assert_eq!(
395 (name1.name(), name1.is_host(), name1.suffix()),
396 ("foo", false, None)
397 );
398 assert_eq!(
399 (name2.name(), name2.is_host(), name2.suffix()),
400 ("foo", false, Some("bar"))
401 );
402 assert_eq!(
403 (name3.name(), name3.is_host(), name3.suffix()),
404 ("foo", true, None)
405 );
406 assert_eq!(
407 (name4.name(), name4.is_host(), name4.suffix()),
408 ("foo", true, Some(""))
409 );
410 Ok(())
411 }
412
413 #[test]
414 fn deserialize_with_depends() -> Result<(), toml::de::Error> {
415 let actual = Package::from_toml(WORKING_DEPENDS)?;
416 let expected = Package {
417 name: PackageName("gzdoom".into()),
418 version: "TODO".into(),
419 target: "x86_64-unknown-redox".into(),
420 depends: vec![
421 PackageName("gtk3".into()),
422 PackageName("sdl2".into()),
423 PackageName("zmusic".into()),
424 ],
425 ..Default::default()
426 };
427
428 assert_eq!(expected, actual);
429 Ok(())
430 }
431
432 #[test]
433 fn deserialize_no_depends() -> Result<(), toml::de::Error> {
434 let actual = Package::from_toml(WORKING_NO_DEPENDS)?;
435 let expected = Package {
436 name: PackageName("kmquake2".into()),
437 version: "TODO".into(),
438 target: "x86_64-unknown-redox".into(),
439 ..Default::default()
440 };
441
442 assert_eq!(expected, actual);
443 Ok(())
444 }
445
446 #[test]
447 fn deserialize_empty_depends() -> Result<(), toml::de::Error> {
448 let actual = Package::from_toml(WORKING_EMPTY_DEPENDS)?;
449 let expected = Package {
450 name: PackageName("iodoom3".into()),
451 version: "TODO".into(),
452 target: "x86_64-unknown-redox".into(),
453 depends: vec![],
454 ..Default::default()
455 };
456
457 assert_eq!(expected, actual);
458 Ok(())
459 }
460
461 #[test]
462 fn deserialize_empty_version() -> Result<(), toml::de::Error> {
463 let actual = Package::from_toml(WORKING_EMPTY_VERSION)?;
464 let expected = Package {
465 name: PackageName("dev-essentials".into()),
466 target: "x86_64-unknown-redox".into(),
467 depends: vec![PackageName("gcc13".into())],
468 ..Default::default()
469 };
470
471 assert_eq!(expected, actual);
472 Ok(())
473 }
474
475 #[test]
476 fn deserialize_repository() -> Result<(), toml::de::Error> {
477 let actual = Repository::from_toml(WORKING_REPOSITORY)?;
478 let expected = Repository {
479 packages: BTreeMap::from([("foo".into(), "bar".into())]),
480 };
481
482 assert_eq!(expected, actual);
483 Ok(())
484 }
485
486 #[test]
487 #[should_panic]
488 fn deserialize_with_invalid_name_fails() {
489 Package::from_toml(INVALID_NAME).unwrap();
490 }
491
492 #[test]
493 #[should_panic]
494 fn deserialize_with_invalid_dependency_name_fails() {
495 Package::from_toml(INVALID_NAME_DEPENDS).unwrap();
496 }
497
498 #[test]
499 fn roundtrip() -> Result<(), toml::de::Error> {
500 let package = Package::from_toml(WORKING_DEPENDS)?;
501 let package_roundtrip = Package::from_toml(&package.to_toml())?;
502
503 assert_eq!(package, package_roundtrip);
504 Ok(())
505 }
506}