1use std::{
2 borrow::Borrow,
3 collections::{HashMap, 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
16#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)]
17pub struct Package {
18 pub name: PackageName,
19 #[serde(default, skip_serializing_if = "String::is_empty")]
20 pub version: String,
21 pub target: String,
22 #[serde(default)]
25 pub depends: Vec<PackageName>,
26}
27
28impl Package {
29 pub fn new(name: &PackageName) -> Result<Self, PackageError> {
30 let dir = find(name.as_str()).ok_or_else(|| PackageError::PackageNotFound(name.clone()))?;
31 let target = env::var("TARGET").map_err(|_| PackageError::TargetInvalid)?;
32
33 let file = dir.join("target").join(target).join("stage.toml");
34 if !file.is_file() {
35 return Err(PackageError::FileMissing(file));
36 }
37
38 let toml = fs::read_to_string(&file)
39 .map_err(|err| PackageError::Parse(DeError::custom(err), Some(file.clone())))?;
40 toml::from_str(&toml).map_err(|err| PackageError::Parse(DeError::custom(err), Some(file)))
41 }
42
43 pub fn new_recursive(
44 names: &[PackageName],
45 recursion: usize,
46 ) -> Result<Vec<Self>, PackageError> {
47 if recursion == 0 {
48 return Err(PackageError::Recursion(Default::default()));
49 }
50
51 let mut packages = Vec::new();
52 for name in names {
53 let package = Self::new(name)?;
54
55 let dependencies =
56 Self::new_recursive(&package.depends, recursion - 1).map_err(|mut err| {
57 err.append_recursion(name);
58 err
59 })?;
60
61 for dependency in dependencies {
62 if !packages.contains(&dependency) {
63 packages.push(dependency);
64 }
65 }
66
67 if !packages.contains(&package) {
68 packages.push(package);
69 }
70 }
71
72 Ok(packages)
73 }
74
75 pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
76 from_str(text)
77 }
78
79 #[allow(dead_code)]
80 pub fn to_toml(&self) -> String {
81 to_string(self).unwrap()
84 }
85}
86
87#[derive(Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
88#[serde(into = "String")]
89#[serde(try_from = "String")]
90pub struct PackageName(String);
91
92impl PackageName {
93 pub fn new(name: impl Into<String>) -> Result<Self, PackageError> {
94 let name = name.into();
95 if name.is_empty() || name.contains(['.', '/', '\0']) {
97 return Err(PackageError::PackageNameInvalid(name));
98 }
99 Ok(Self(name))
100 }
101
102 pub fn as_str(&self) -> &str {
103 self.0.as_str()
104 }
105}
106
107impl From<PackageName> for String {
108 fn from(package_name: PackageName) -> Self {
109 package_name.0
110 }
111}
112
113impl TryFrom<String> for PackageName {
114 type Error = PackageError;
115 fn try_from(name: String) -> Result<Self, Self::Error> {
116 Self::new(name)
117 }
118}
119
120impl TryFrom<&str> for PackageName {
121 type Error = PackageError;
122 fn try_from(name: &str) -> Result<Self, Self::Error> {
123 Self::new(name)
124 }
125}
126
127impl TryFrom<&OsStr> for PackageName {
128 type Error = PackageError;
129 fn try_from(name: &OsStr) -> Result<Self, Self::Error> {
130 let name = name
131 .to_str()
132 .ok_or_else(|| PackageError::PackageNameInvalid(name.to_string_lossy().to_string()))?;
133 Self::new(name)
134 }
135}
136
137impl TryFrom<OsString> for PackageName {
138 type Error = PackageError;
139 fn try_from(name: OsString) -> Result<Self, Self::Error> {
140 name.as_os_str().try_into()
141 }
142}
143
144impl fmt::Display for PackageName {
145 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
146 write!(f, "{}", self.0)
147 }
148}
149
150impl Borrow<str> for PackageName {
151 fn borrow(&self) -> &str {
152 self.as_str()
153 }
154}
155
156#[derive(Debug)]
157pub struct PackageInfo {
158 pub installed: bool,
159 pub version: String,
160 pub target: String,
161
162 pub download_size: String,
163 pub depends: Vec<PackageName>,
165}
166
167#[derive(Debug, serde::Deserialize)]
168pub struct Repository {
169 pub packages: HashMap<String, String>,
170}
171
172impl Repository {
173 pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
174 from_str(text)
175 }
176}
177
178#[derive(Debug, thiserror::Error)]
182pub enum PackageError {
183 #[error("Missing package file {0:?}")]
184 FileMissing(PathBuf),
185 #[error("Package {0:?} name invalid")]
186 PackageNameInvalid(String),
187 #[error("Package {0:?} not found")]
188 PackageNotFound(PackageName),
189 #[error("Failed parsing package: {0}; file: {1:?}")]
190 Parse(serde::de::value::Error, Option<PathBuf>),
191 #[error("Recursion limit reached while processing dependencies; tree: {0:?}")]
192 Recursion(VecDeque<PackageName>),
193 #[error("TARGET triplet env var unset or invalid")]
194 TargetInvalid,
195}
196
197impl PackageError {
198 pub fn append_recursion(&mut self, name: &PackageName) {
204 if let PackageError::Recursion(ref mut packages) = self {
205 packages.push_front(name.clone());
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::{Package, PackageName};
213
214 const WORKING_DEPENDS: &str = r#"
215 name = "gzdoom"
216 version = "TODO"
217 target = "x86_64-unknown-redox"
218 depends = ["gtk3", "sdl2", "zmusic"]
219 "#;
220
221 const WORKING_NO_DEPENDS: &str = r#"
222 name = "kmquake2"
223 version = "TODO"
224 target = "x86_64-unknown-redox"
225 "#;
226
227 const WORKING_EMPTY_DEPENDS: &str = r#"
228 name = "iodoom3"
229 version = "TODO"
230 target = "x86_64-unknown-redox"
231 depends = []
232 "#;
233
234 const WORKING_EMPTY_VERSION: &str = r#"
235 name = "dev-essentials"
236 target = "x86_64-unknown-redox"
237 depends = ["gcc13"]
238 "#;
239
240 const INVALID_NAME: &str = r#"
241 name = "dolphin.emulator"
242 version = "TODO"
243 target = "x86_64-unknown-redox"
244 depends = ["qt5"]
245 "#;
246
247 const INVALID_NAME_DEPENDS: &str = r#"
248 name = "mgba"
249 version = "TODO"
250 target = "x86_64-unknown-redox"
251 depends = ["ffmpeg.latest"]
252 "#;
253
254 #[test]
255 fn deserialize_with_depends() -> Result<(), toml::de::Error> {
256 let actual = Package::from_toml(WORKING_DEPENDS)?;
257 let expected = Package {
258 name: PackageName("gzdoom".into()),
259 version: "TODO".into(),
260 target: "x86_64-unknown-redox".into(),
261 depends: vec![
262 PackageName("gtk3".into()),
263 PackageName("sdl2".into()),
264 PackageName("zmusic".into()),
265 ],
266 };
267
268 assert_eq!(expected, actual);
269 Ok(())
270 }
271
272 #[test]
273 fn deserialize_no_depends() -> Result<(), toml::de::Error> {
274 let actual = Package::from_toml(WORKING_NO_DEPENDS)?;
275 let expected = Package {
276 name: PackageName("kmquake2".into()),
277 version: "TODO".into(),
278 target: "x86_64-unknown-redox".into(),
279 depends: vec![],
280 };
281
282 assert_eq!(expected, actual);
283 Ok(())
284 }
285
286 #[test]
287 fn deserialize_empty_depends() -> Result<(), toml::de::Error> {
288 let actual = Package::from_toml(WORKING_EMPTY_DEPENDS)?;
289 let expected = Package {
290 name: PackageName("iodoom3".into()),
291 version: "TODO".into(),
292 target: "x86_64-unknown-redox".into(),
293 depends: vec![],
294 };
295
296 assert_eq!(expected, actual);
297 Ok(())
298 }
299
300 #[test]
301 fn deserialize_empty_version() -> Result<(), toml::de::Error> {
302 let actual = Package::from_toml(WORKING_EMPTY_VERSION)?;
303 let expected = Package {
304 name: PackageName("dev-essentials".into()),
305 version: "".into(),
306 target: "x86_64-unknown-redox".into(),
307 depends: vec![PackageName("gcc13".into())],
308 };
309
310 assert_eq!(expected, actual);
311 Ok(())
312 }
313
314 #[test]
315 #[should_panic]
316 fn deserialize_with_invalid_name_fails() {
317 Package::from_toml(INVALID_NAME).unwrap();
318 }
319
320 #[test]
321 #[should_panic]
322 fn deserialize_with_invalid_dependency_name_fails() {
323 Package::from_toml(INVALID_NAME_DEPENDS).unwrap();
324 }
325
326 #[test]
327 fn roundtrip() -> Result<(), toml::de::Error> {
328 let package = Package::from_toml(WORKING_DEPENDS)?;
329 let package_roundtrip = Package::from_toml(&package.to_toml())?;
330
331 assert_eq!(package, package_roundtrip);
332 Ok(())
333 }
334}