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
20pub type RemoteName = String;
22
23#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd)]
24pub struct RemotePackage {
25 pub package: Package,
26 pub remote: RemoteName,
27}
28
29#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, PartialOrd)]
30#[serde(default)]
31pub struct Package {
32 pub name: PackageName,
34 #[serde(skip_serializing_if = "String::is_empty")]
36 pub version: String,
37 pub target: String,
39 #[serde(skip_serializing_if = "String::is_empty")]
41 pub blake3: String,
42 #[serde(skip_serializing_if = "String::is_empty")]
44 pub source_identifier: String,
45 #[serde(skip_serializing_if = "String::is_empty")]
47 pub commit_identifier: String,
48 #[serde(skip_serializing_if = "String::is_empty")]
50 pub time_identifier: String,
51 #[serde(skip_serializing_if = "is_zero")]
53 pub storage_size: u64,
54 #[serde(skip_serializing_if = "is_zero")]
56 pub network_size: u64,
57 pub depends: Vec<PackageName>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum PackagePrefix {
63 Any,
64 Host,
65 Target,
66}
67
68impl Package {
69 pub fn new(name: &PackageName) -> Result<Self, PackageError> {
70 let dir = find(name.name()).ok_or_else(|| PackageError::PackageNotFound(name.clone()))?;
71 let target = env::var("TARGET").map_err(|_| PackageError::TargetInvalid)?;
72
73 let file = dir.join("target").join(target).join("stage.toml");
74 if !file.is_file() {
75 return Err(PackageError::FileMissing(file));
76 }
77
78 let toml = fs::read_to_string(&file)
79 .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?;
80 toml::from_str(&toml).map_err(|err| PackageError::Parse(DeError::custom(err), Some(file)))
81 }
82
83 pub fn new_recursive(
84 names: &[PackageName],
85 nonstop: bool,
86 recursion: usize,
87 ) -> Result<Vec<Self>, PackageError> {
88 if names.len() == 0 {
89 return Ok(vec![]);
90 }
91 let (list, map) = Self::new_recursive_nonstop(names, recursion);
92 if nonstop && list.len() > 0 {
93 Ok(list)
94 } else if !nonstop && map.len() == list.len() {
95 Ok(list)
96 } else {
97 let (_, res) = map.into_iter().find(|(_, v)| v.is_err()).unwrap();
98 Err(res.err().unwrap())
99 }
100 }
101
102 pub fn new_recursive_nonstop(
105 names: &[PackageName],
106 recursion: usize,
107 ) -> (Vec<Self>, BTreeMap<PackageName, Result<(), PackageError>>) {
108 let mut packages = Vec::new();
109 let mut packages_map = BTreeMap::new();
110 for name in names {
111 if packages_map.contains_key(name) {
112 continue;
113 }
114
115 let package = if recursion == 0 {
116 Err(PackageError::Recursion(Default::default()))
117 } else {
118 Self::new(name)
119 };
120
121 match package {
122 Ok(package) => {
123 let mut has_invalid_dependency = false;
124 let (dependencies, dependencies_map) =
125 Self::new_recursive_nonstop(&package.depends, recursion - 1);
126 for dependency in dependencies {
127 if !packages_map.contains_key(&dependency.name) {
128 packages_map.insert(dependency.name.clone(), Ok(()));
129 packages.push(dependency);
130 }
131 }
132 for (dep_name, result) in dependencies_map {
133 if let Err(mut e) = result {
134 if !packages_map.contains_key(&dep_name) {
135 e.append_recursion(name);
136 packages_map.insert(dep_name, Err(e));
137 }
138 has_invalid_dependency = true;
139 }
140 }
141 if !packages_map.contains_key(name) {
143 packages_map.insert(
144 name.clone(),
145 if has_invalid_dependency {
146 Err(PackageError::DependencyInvalid(name.clone()))
147 } else {
148 Ok(())
149 },
150 );
151 packages.push(package);
152 }
153 }
154 Err(e) => {
155 packages_map.insert(name.clone(), Err(e));
156 }
157 }
158 }
159
160 (packages, packages_map)
161 }
162
163 pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
164 from_str(text)
165 }
166
167 #[allow(dead_code)]
168 pub fn to_toml(&self) -> String {
169 to_string(self).unwrap()
172 }
173}
174
175#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
183#[serde(into = "String")]
184#[serde(try_from = "String")]
185pub struct PackageName(String);
186
187impl PackageName {
188 pub fn new(name: impl Into<String>) -> Result<Self, PackageError> {
189 let name = name.into();
190 if name.is_empty() {
192 return Err(PackageError::PackageNameInvalid(name));
193 }
194 let mut pkg_separator = 0;
195 let mut has_os_prefix = false;
196 for c in name.chars() {
197 if "/\0".contains(c) {
198 return Err(PackageError::PackageNameInvalid(name));
199 }
200 if c == '.' {
201 pkg_separator += 1;
202 if pkg_separator > 1 {
203 return Err(PackageError::PackageNameInvalid(name));
204 }
205 }
206 if c == ':' {
207 if has_os_prefix {
208 return Err(PackageError::PackageNameInvalid(name));
209 }
210 has_os_prefix = true;
211 }
212 }
213 let r = Self(name);
214 if has_os_prefix && !r.is_host() && !r.is_target() {
215 return Err(PackageError::PackageNameInvalid(r.0));
216 }
217 Ok(r)
218 }
219
220 pub fn from_list(vec: Vec<impl Into<String>>) -> Result<Vec<Self>, PackageError> {
221 vec.into_iter().map(|p| Self::new(p)).collect()
222 }
223
224 pub fn as_str(&self) -> &str {
225 self.0.as_str()
226 }
227
228 pub fn is_host(&self) -> bool {
230 self.0.starts_with("host:")
231 }
232
233 pub fn is_target(&self) -> bool {
235 self.0.starts_with("target:")
236 }
237
238 fn strip<'a>(
239 &'a self,
240 strip_os: bool,
241 strip_pkg: bool,
242 ) -> (Option<&'a str>, &'a str, Option<&'a str>) {
243 let mut s = self.0.as_str();
244 let mut os = None;
245 let mut pkg = None;
246 if strip_os {
247 if self.is_host() {
248 os = Some(&s[..4]);
249 s = &s[5..];
250 } else if self.is_target() {
251 os = Some(&s[..6]);
252 s = &s[7..];
253 }
254 }
255 if strip_pkg {
256 if let Some(pos) = s.find('.') {
257 pkg = Some(&s[pos + 1..]);
258 s = &s[..pos];
259 }
260 }
261 (os, s, pkg)
262 }
263
264 pub fn name(&self) -> &str {
266 self.strip(true, true).1
267 }
268
269 pub fn suffix(&self) -> Option<&str> {
271 let s = self.without_host();
272 if let Some(pos) = s.find('.') {
273 Some(&s[pos + 1..])
274 } else {
275 None
276 }
277 }
278
279 pub fn without_host(&self) -> &str {
281 if self.is_host() {
282 &self.as_str()[5..]
283 } else {
284 self.as_str()
285 }
286 }
287
288 pub fn without_target(&self) -> &str {
290 if self.is_target() {
291 &self.as_str()[7..]
292 } else {
293 self.as_str()
294 }
295 }
296
297 pub fn without_prefix(&self) -> &str {
299 let s = self.strip(true, false);
300 s.1
301 }
302
303 pub fn with_host(&self) -> PackageName {
305 self.with_prefix(PackagePrefix::Host)
306 }
307
308 pub fn with_target(&self) -> PackageName {
310 self.with_prefix(PackagePrefix::Target)
311 }
312
313 pub fn with_prefix(&self, os: PackagePrefix) -> PackageName {
315 let name = self.strip(true, false).1;
316 let name = match os {
317 PackagePrefix::Any => name.to_string(),
318 PackagePrefix::Host => format!("host:{}", name),
319 PackagePrefix::Target => format!("target:{}", name),
320 };
321
322 Self(name)
323 }
324
325 pub fn with_prefixed_suffix(&self, suffix: Option<&str>) -> PackageName {
327 let mut name = self.strip(false, true).1.to_string();
328 if let Some(suffix) = suffix {
329 name.push('.');
330 name.push_str(suffix);
331 }
332
333 Self(name)
334 }
335
336 pub fn with_suffix(&self, suffix: Option<&str>) -> PackageName {
338 let mut name = self.strip(true, true).1.to_string();
339 if let Some(suffix) = suffix {
340 name.push('.');
341 name.push_str(suffix);
342 }
343
344 Self(name)
345 }
346}
347
348impl From<PackageName> for String {
349 fn from(package_name: PackageName) -> Self {
350 package_name.0
351 }
352}
353
354impl TryFrom<String> for PackageName {
355 type Error = PackageError;
356 fn try_from(name: String) -> Result<Self, Self::Error> {
357 Self::new(name)
358 }
359}
360
361impl TryFrom<&str> for PackageName {
362 type Error = PackageError;
363 fn try_from(name: &str) -> Result<Self, Self::Error> {
364 Self::new(name)
365 }
366}
367
368impl TryFrom<&OsStr> for PackageName {
369 type Error = PackageError;
370 fn try_from(name: &OsStr) -> Result<Self, Self::Error> {
371 let name = name
372 .to_str()
373 .ok_or_else(|| PackageError::PackageNameInvalid(name.to_string_lossy().to_string()))?;
374 Self::new(name)
375 }
376}
377
378impl TryFrom<OsString> for PackageName {
379 type Error = PackageError;
380 fn try_from(name: OsString) -> Result<Self, Self::Error> {
381 name.as_os_str().try_into()
382 }
383}
384
385impl fmt::Display for PackageName {
386 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
387 write!(f, "{}", self.0)
388 }
389}
390
391impl Borrow<str> for PackageName {
392 fn borrow(&self) -> &str {
393 self.as_str()
394 }
395}
396
397#[derive(Debug)]
398pub struct PackageInfo {
399 pub installed: bool,
400 pub package: RemotePackage,
401}
402
403#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
404#[serde(default)]
405pub struct SourceIdentifier {
406 #[serde(skip_serializing_if = "String::is_empty")]
408 pub source_identifier: String,
409 #[serde(skip_serializing_if = "String::is_empty")]
411 pub commit_identifier: String,
412 #[serde(skip_serializing_if = "String::is_empty")]
414 pub time_identifier: String,
415}
416
417#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
418#[serde(default)]
419pub struct Repository {
420 pub packages: BTreeMap<String, String>,
422 pub outdated_packages: BTreeMap<String, SourceIdentifier>,
424}
425
426impl Repository {
427 pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
428 from_str(text)
429 }
430}
431
432#[derive(Clone, Debug, thiserror::Error)]
436pub enum PackageError {
437 #[error("Missing package file {0:?}")]
438 FileMissing(PathBuf),
439 #[error("Package {0:?} name invalid")]
440 PackageNameInvalid(String),
441 #[error("Package {0:?} not found")]
442 PackageNotFound(PackageName),
443 #[error("Failed parsing package: {0}; file: {1:?}")]
444 Parse(serde::de::value::Error, Option<PathBuf>),
445 #[error("Recursion limit reached while processing dependencies; tree: {0:?}")]
446 Recursion(VecDeque<PackageName>),
447 #[error("Package {0:?} is missing one or more dependencies")]
448 DependencyInvalid(PackageName),
449 #[error("TARGET triplet env var unset or invalid")]
450 TargetInvalid,
451}
452
453impl PackageError {
454 pub fn append_recursion(&mut self, name: &PackageName) {
460 if let PackageError::Recursion(ref mut packages) = self {
461 packages.push_front(name.clone());
462 }
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use std::collections::BTreeMap;
469
470 use crate::package::{Repository, SourceIdentifier};
471
472 use super::{Package, PackageName};
473
474 const WORKING_DEPENDS: &str = r#"
475 name = "gzdoom"
476 version = "TODO"
477 target = "x86_64-unknown-redox"
478 depends = ["gtk3", "sdl2", "zmusic"]
479 "#;
480
481 const WORKING_NO_DEPENDS: &str = r#"
482 name = "kmquake2"
483 version = "TODO"
484 target = "x86_64-unknown-redox"
485 "#;
486
487 const WORKING_EMPTY_DEPENDS: &str = r#"
488 name = "iodoom3"
489 version = "TODO"
490 target = "x86_64-unknown-redox"
491 depends = []
492 "#;
493
494 const WORKING_EMPTY_VERSION: &str = r#"
495 name = "dev-essentials"
496 target = "x86_64-unknown-redox"
497 depends = ["gcc13"]
498 "#;
499
500 const WORKING_REPOSITORY: &str = r#"
501 [packages]
502 foo = "bar"
503 "#;
504
505 const WORKING_OUTDATED_REPOSITORY: &str = r#"
506 [outdated_packages.gnu-make]
507 source_identifier = "1a0e5353205e106bd9b3c0f4a5f37ee1156a1e1c8feb771d1b4842c216612cba"
508 commit_identifier = "da93b635fec96a6fac7da9bf7742d850cbce68b4"
509 time_identifier = "2025-12-13T05:33:07Z"
510 "#;
511
512 const INVALID_NAME: &str = r#"
513 name = "dolphin.emu.lator"
514 version = "TODO"
515 target = "x86_64-unknown-redox"
516 depends = ["qt5"]
517 "#;
518
519 const INVALID_NAME_DEPENDS: &str = r#"
520 name = "mgba"
521 version = "TODO"
522 target = "x86_64-unknown-redox"
523 depends = ["ffmpeg:latest"]
524 "#;
525
526 #[test]
527 fn package_name_split() -> Result<(), toml::de::Error> {
528 let name1 = PackageName::new("foo").unwrap();
529 let name2 = PackageName::new("foo.bar").unwrap();
530 let name3 = PackageName::new("host:foo").unwrap();
531 let name4 = PackageName::new("host:foo.").unwrap();
532 assert_eq!(
533 (name1.name(), name1.is_host(), name1.suffix()),
534 ("foo", false, None)
535 );
536 assert_eq!(
537 (name2.name(), name2.is_host(), name2.suffix()),
538 ("foo", false, Some("bar"))
539 );
540 assert_eq!(
541 (name3.name(), name3.is_host(), name3.suffix()),
542 ("foo", true, None)
543 );
544 assert_eq!(
545 (name4.name(), name4.is_host(), name4.suffix()),
546 ("foo", true, Some(""))
547 );
548 Ok(())
549 }
550
551 #[test]
552 fn deserialize_with_depends() -> Result<(), toml::de::Error> {
553 let actual = Package::from_toml(WORKING_DEPENDS)?;
554 let expected = Package {
555 name: PackageName("gzdoom".into()),
556 version: "TODO".into(),
557 target: "x86_64-unknown-redox".into(),
558 depends: vec![
559 PackageName("gtk3".into()),
560 PackageName("sdl2".into()),
561 PackageName("zmusic".into()),
562 ],
563 ..Default::default()
564 };
565
566 assert_eq!(expected, actual);
567 Ok(())
568 }
569
570 #[test]
571 fn deserialize_no_depends() -> Result<(), toml::de::Error> {
572 let actual = Package::from_toml(WORKING_NO_DEPENDS)?;
573 let expected = Package {
574 name: PackageName("kmquake2".into()),
575 version: "TODO".into(),
576 target: "x86_64-unknown-redox".into(),
577 ..Default::default()
578 };
579
580 assert_eq!(expected, actual);
581 Ok(())
582 }
583
584 #[test]
585 fn deserialize_empty_depends() -> Result<(), toml::de::Error> {
586 let actual = Package::from_toml(WORKING_EMPTY_DEPENDS)?;
587 let expected = Package {
588 name: PackageName("iodoom3".into()),
589 version: "TODO".into(),
590 target: "x86_64-unknown-redox".into(),
591 depends: vec![],
592 ..Default::default()
593 };
594
595 assert_eq!(expected, actual);
596 Ok(())
597 }
598
599 #[test]
600 fn deserialize_empty_version() -> Result<(), toml::de::Error> {
601 let actual = Package::from_toml(WORKING_EMPTY_VERSION)?;
602 let expected = Package {
603 name: PackageName("dev-essentials".into()),
604 target: "x86_64-unknown-redox".into(),
605 depends: vec![PackageName("gcc13".into())],
606 ..Default::default()
607 };
608
609 assert_eq!(expected, actual);
610 Ok(())
611 }
612
613 #[test]
614 fn deserialize_repository() -> Result<(), toml::de::Error> {
615 let actual = Repository::from_toml(WORKING_REPOSITORY)?;
616 let expected = Repository {
617 packages: BTreeMap::from([("foo".into(), "bar".into())]),
618 ..Default::default()
619 };
620
621 assert_eq!(expected, actual);
622 Ok(())
623 }
624
625 #[test]
626 fn deserialize_repository_outdated() -> Result<(), toml::de::Error> {
627 let actual = Repository::from_toml(WORKING_OUTDATED_REPOSITORY)?;
628 let expected = Repository {
629 outdated_packages: BTreeMap::from([(
630 "gnu-make".into(),
631 SourceIdentifier {
632 source_identifier:
633 "1a0e5353205e106bd9b3c0f4a5f37ee1156a1e1c8feb771d1b4842c216612cba".into(),
634 commit_identifier: "da93b635fec96a6fac7da9bf7742d850cbce68b4".into(),
635 time_identifier: "2025-12-13T05:33:07Z".into(),
636 },
637 )]),
638 ..Default::default()
639 };
640
641 assert_eq!(expected, actual);
642 Ok(())
643 }
644
645 #[test]
646 #[should_panic]
647 fn deserialize_with_invalid_name_fails() {
648 Package::from_toml(INVALID_NAME).unwrap();
649 }
650
651 #[test]
652 #[should_panic]
653 fn deserialize_with_invalid_dependency_name_fails() {
654 Package::from_toml(INVALID_NAME_DEPENDS).unwrap();
655 }
656
657 #[test]
658 fn roundtrip() -> Result<(), toml::de::Error> {
659 let package = Package::from_toml(WORKING_DEPENDS)?;
660 let package_roundtrip = Package::from_toml(&package.to_toml())?;
661
662 assert_eq!(package, package_roundtrip);
663 Ok(())
664 }
665}