1use serde::{Deserialize, Serialize};
2use std::collections::{BTreeMap, HashMap};
3use std::fs;
4use std::io::ErrorKind;
5use std::path::PathBuf;
6use tracing::{debug, instrument, trace};
7
8use crate::error::ProjectError;
9
10#[derive(Deserialize, Serialize, Debug, Clone)]
11#[serde_with::skip_serializing_none]
12pub struct ProjectData {
13 pub manifest: HashMap<String, String>,
14}
15
16#[derive(Deserialize, Serialize, Debug, Clone)]
17#[serde_with::skip_serializing_none]
18pub struct Manifest {
19 pub project: BTreeMap<String, Project>,
20 #[serde(default)]
21 pub config: Config,
22}
23
24#[derive(Deserialize, Serialize, Debug, Clone, Default)]
25#[serde_with::skip_serializing_none]
26pub struct Config {
27 pub mock_config: Option<String>,
28 pub strip_prefix: Option<String>,
29 pub strip_suffix: Option<String>,
30 pub project_regex: Option<String>,
31}
32
33impl Manifest {
34 #[must_use]
35 pub fn find_key_for_value(&self, value: &Project) -> Option<&String> {
36 self.project.iter().find_map(|(key, val)| (val == value).then_some(key))
37 }
38
39 #[must_use]
40 pub fn get_project(&self, key: &str) -> Option<&Project> {
41 self.project.get(key).map_or_else(
42 || {
43 self.project.iter().find_map(|(_k, v)| {
44 let alias = v.alias.as_ref()?;
45 alias.contains(&key.to_owned()).then_some(v)
46 })
47 },
48 Some,
49 )
50 }
51}
52
53#[derive(Deserialize, PartialEq, Eq, Serialize, Debug, Clone, Default)]
54#[serde_with::skip_serializing_none]
55pub struct Project {
56 pub rpm: Option<RpmBuild>,
57 pub podman: Option<Docker>,
58 pub docker: Option<Docker>,
59 pub flatpak: Option<Flatpak>,
60 pub pre_script: Option<PathBuf>,
61 pub post_script: Option<PathBuf>,
62 pub env: Option<BTreeMap<String, String>>,
63 pub alias: Option<Vec<String>>,
64 pub scripts: Option<Vec<PathBuf>>,
65 #[serde(default)]
66 #[serde(deserialize_with = "btree_wild_string")]
67 pub labels: BTreeMap<String, String>,
68 pub update: Option<PathBuf>,
69 pub arches: Option<Vec<String>>,
70}
71
72fn btree_wild_string<'de, D>(deserializer: D) -> Result<BTreeMap<String, String>, D::Error>
78where
79 D: serde::Deserializer<'de>,
80{
81 struct WildString;
82
83 impl serde::de::Visitor<'_> for WildString {
84 type Value = String;
85
86 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
87 formatter.write_str("string, integer, bool or unit")
88 }
89
90 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
91 where
92 E: serde::de::Error,
93 {
94 Ok(v)
95 }
96
97 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
98 where
99 E: serde::de::Error,
100 {
101 Ok(v.to_owned())
102 }
103
104 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
105 where
106 E: serde::de::Error,
107 {
108 Ok(format!("{v}"))
109 }
110
111 fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E>
112 where
113 E: serde::de::Error,
114 {
115 Ok(format!("{v}"))
116 }
117
118 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
119 where
120 E: serde::de::Error,
121 {
122 Ok(format!("{v}"))
123 }
124
125 fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E>
126 where
127 E: serde::de::Error,
128 {
129 Ok(format!("{v}"))
130 }
131
132 fn visit_unit<E>(self) -> Result<Self::Value, E>
133 where
134 E: serde::de::Error,
135 {
136 Ok(String::new())
137 }
138
139 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
140 where
141 E: serde::de::Error,
142 {
143 Ok(format!("{v}"))
144 }
145 }
146
147 struct RealWildString(String);
148
149 impl<'de> Deserialize<'de> for RealWildString {
150 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
151 where
152 D: serde::Deserializer<'de>,
153 {
154 deserializer.deserialize_any(WildString).map(Self)
155 }
156 }
157
158 struct BTreeWildStringVisitor;
159
160 impl<'de> serde::de::Visitor<'de> for BTreeWildStringVisitor {
161 type Value = BTreeMap<String, String>;
162
163 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
164 formatter.write_str("map (key: string, value: wild string)")
165 }
166
167 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
168 where
169 A: serde::de::MapAccess<'de>,
170 {
171 let mut res = Self::Value::new();
172 while let Some((k, v)) = map.next_entry::<String, RealWildString>()? {
173 res.insert(k, v.0);
174 }
175 Ok(res)
176 }
177 }
178
179 deserializer.deserialize_map(BTreeWildStringVisitor)
180}
181
182#[derive(Deserialize, PartialEq, Eq, Serialize, Debug, Clone, Default)]
183#[serde_with::skip_serializing_none]
184pub struct RpmBuild {
185 pub spec: PathBuf,
186 pub sources: Option<PathBuf>,
187 pub package: Option<String>,
188 pub pre_script: Option<PathBuf>,
189 pub post_script: Option<PathBuf>,
190 pub enable_scm: Option<bool>,
191 #[serde(default)]
192 pub extra_repos: Vec<String>,
193 pub scm_opts: Option<BTreeMap<String, String>>,
194 pub config: Option<BTreeMap<String, String>>,
195 pub mock_config: Option<String>,
196 pub plugin_opts: Option<BTreeMap<String, String>>,
197 pub macros: Option<BTreeMap<String, String>>,
198 pub opts: Option<BTreeMap<String, String>>,
199}
200
201#[derive(Deserialize, PartialEq, Eq, Serialize, Debug, Clone, Default)]
202#[serde_with::skip_serializing_none]
203pub struct Docker {
204 pub image: BTreeMap<String, DockerImage>, }
206
207pub fn parse_kv(input: &str) -> impl Iterator<Item = Option<(String, String)>> + '_ {
208 input
209 .split(',')
210 .filter(|item| !item.trim().is_empty())
211 .map(|item| item.split_once('=').map(|(l, r)| (l.to_owned(), r.to_owned())))
212}
213
214pub fn parse_filters(filters: &[String]) -> Option<Vec<Vec<(String, String)>>> {
215 filters.iter().map(std::ops::Deref::deref).map(crate::parse_kv).map(Iterator::collect).collect()
216}
217
218pub fn parse_labels<'a, I: Iterator<Item = &'a str>>(labels: I) -> Option<Vec<(String, String)>> {
220 labels.flat_map(parse_kv).collect()
221}
222
223#[derive(Deserialize, PartialEq, Eq, Serialize, Debug, Clone, Default)]
224#[serde_with::skip_serializing_none]
225pub struct DockerImage {
226 pub dockerfile: Option<String>,
227 pub import: Option<PathBuf>,
228 pub tag_latest: Option<bool>,
229 pub context: String,
230 pub version: Option<String>,
231}
232
233#[derive(Deserialize, PartialEq, Eq, Serialize, Debug, Clone)]
234#[serde_with::skip_serializing_none]
235pub struct Flatpak {
236 pub manifest: PathBuf,
237 pub pre_script: Option<PathBuf>,
238 pub post_script: Option<PathBuf>,
239}
240
241pub fn to_string(config: &Manifest) -> Result<String, hcl::Error> {
246 let config = hcl::to_string(&config)?;
247 Ok(config)
248}
249
250#[instrument]
251pub fn load_from_file(path: &PathBuf) -> Result<Manifest, ProjectError> {
252 debug!("Reading hcl file: {path:?}");
253 let file = fs::read_to_string(path).map_err(|e| match e.kind() {
254 ErrorKind::NotFound => ProjectError::NoManifest,
255 _ => ProjectError::InvalidManifest(e.to_string()),
256 })?;
257
258 debug!("Loading config from {path:?}");
259 let mut config = load_from_string(&file)?;
260
261 let parent = if path.parent().unwrap().as_os_str().is_empty() {
265 PathBuf::from(".")
266 } else {
267 path.parent().unwrap().to_path_buf()
268 };
269
270 let walk = ignore::Walk::new(parent);
271
272 let path = path.canonicalize().expect("Invalid path");
273
274 for entry in walk {
275 trace!("Found {entry:?}");
276 let entry = entry.unwrap();
277
278 if entry.path() == path {
280 continue;
281 }
282
283 if entry.file_type().unwrap().is_file() && entry.path().file_name().unwrap() == "anda.hcl" {
284 debug!("Loading: {entry:?}");
285 let readfile = fs::read_to_string(entry.path())
286 .map_err(|e| ProjectError::InvalidManifest(e.to_string()))?;
287
288 let en = entry.path().parent().unwrap();
289
290 let nested_config = prefix_config(
291 load_from_string(&readfile)?,
292 &en.strip_prefix("./").unwrap_or(en).display().to_string(),
293 );
294 config.project.extend(nested_config.project);
296 }
297 }
298
299 trace!("Loaded config: {config:#?}");
300 generate_alias(&mut config);
301
302 check_config(config)
303}
304
305#[must_use]
306pub fn prefix_config(mut config: Manifest, prefix: &str) -> Manifest {
307 let mut new_config = config.clone();
308
309 for (project_name, project) in &mut config.project {
310 let new_project_name = format!("{prefix}/{project_name}");
312 let mut new_project = std::mem::take(project);
314
315 macro_rules! default {
316 ($o:expr, $attr:ident, $d:expr) => {
317 if let Some($attr) = &mut $o.$attr {
318 if $attr.as_os_str().is_empty() {
319 *$attr = $d.into();
320 }
321 *$attr = PathBuf::from(format!("{prefix}/{}", $attr.display()));
322 } else {
323 let p = PathBuf::from(format!("{prefix}/{}", $d));
324 if p.exists() {
325 $o.$attr = Some(p);
326 }
327 }
328 };
329 } if let Some(rpm) = &mut new_project.rpm {
331 rpm.spec = PathBuf::from(format!("{prefix}/{}", rpm.spec.display()));
332 default!(rpm, pre_script, "rpm_pre.rhai");
333 default!(rpm, post_script, "rpm_post.rhai");
334 default!(rpm, sources, ".");
335 }
336 default!(new_project, update, "update.rhai");
337 default!(new_project, pre_script, "pre.rhai");
338 default!(new_project, post_script, "post.rhai");
339
340 if let Some(scripts) = &mut new_project.scripts {
341 for scr in scripts {
342 *scr = PathBuf::from(format!("{prefix}/{}", scr.display()));
343 }
344 }
345
346 new_config.project.remove(project_name);
347 new_config.project.insert(new_project_name, new_project);
348 }
349 generate_alias(&mut new_config);
350 new_config
351}
352
353pub fn generate_alias(config: &mut Manifest) {
354 fn append_vec(vec: &mut Option<Vec<String>>, value: String) {
355 if let Some(vec) = vec {
356 if vec.contains(&value) {
357 return;
358 }
359
360 vec.push(value);
361 } else {
362 *vec = Some(vec![value]);
363 }
364 }
365
366 for (name, project) in &mut config.project {
367 #[allow(clippy::assigning_clones)]
368 if config.config.strip_prefix.is_some() || config.config.strip_suffix.is_some() {
369 let mut new_name = name.clone();
370 if let Some(strip_prefix) = &config.config.strip_prefix {
371 new_name = new_name.strip_prefix(strip_prefix).unwrap_or(&new_name).to_owned();
372 }
373 if let Some(strip_suffix) = &config.config.strip_suffix {
374 new_name = new_name.strip_suffix(strip_suffix).unwrap_or(&new_name).to_owned();
375 }
376
377 if name != &new_name {
378 append_vec(&mut project.alias, new_name);
379 }
380 }
381 }
382}
383
384#[instrument]
385pub fn load_from_string(config: &str) -> Result<Manifest, ProjectError> {
386 trace!(config, "Dump config");
387 let mut config: Manifest = hcl::eval::from_str(config, &crate::context::hcl_context())?;
388
389 generate_alias(&mut config);
390
391 check_config(config)
392}
393
394pub const fn check_config(config: Manifest) -> Result<Manifest, ProjectError> {
399 Ok(config)
401}
402
403#[allow(clippy::indexing_slicing)]
404#[cfg(test)]
405mod test_parser {
406 use super::*;
407
408 #[test]
409 fn test_parse() {
410 std::env::set_var("RUST_LOG", "trace");
412 env_logger::init();
413 let config = r#"
414 hello = "world"
415 project "anda" {
416 pre_script {
417 commands = [
418 "echo '${env.RUST_LOG}'",
419 ]
420 }
421 labels {
422 nightly = 1
423 }
424 }
425 "#;
426
427 let body = hcl::parse(config).unwrap();
428
429 print!("{body:#?}");
430
431 let config = load_from_string(config).unwrap();
432
433 println!("{config:#?}");
434
435 assert_eq!(config.project["anda"].labels.get("nightly"), Some(&"1".to_owned()));
436 }
437
438 #[test]
439 fn test_map() {
440 let m = [("foo".to_owned(), "bar".to_owned())].into();
441
442 assert_eq!(parse_labels(std::iter::once("foo=bar")), Some(m));
443
444 let multieq = [("foo".to_owned(), "bar=baz".to_owned())].into();
445
446 assert_eq!(parse_labels(std::iter::once("foo=bar=baz")), Some(multieq));
447
448 let multi =
449 [("foo".to_owned(), "bar".to_owned()), ("baz".to_owned(), "qux".to_owned())].into();
450
451 assert_eq!(parse_labels(std::iter::once("foo=bar,baz=qux")), Some(multi));
452 }
453}