omicron_zone_package/config/
imp.rs1use crate::package::{Package, PackageOutput, PackageSource};
8use crate::target::TargetMap;
9use serde_derive::Deserialize;
10use serde_derive::Serialize;
11use std::collections::BTreeMap;
12use std::path::Path;
13use thiserror::Error;
14use topological_sort::TopologicalSort;
15
16use super::{PackageName, PresetName};
17
18pub struct PackageMap<'a>(pub BTreeMap<&'a PackageName, &'a Package>);
22
23#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
25struct OutputFile(String);
26
27impl<'a> PackageMap<'a> {
28 pub fn build_order(&self) -> PackageDependencyIter<'a> {
29 let lookup_by_output = self
30 .0
31 .iter()
32 .map(|(name, package)| (OutputFile(package.get_output_file(name)), (*name, *package)))
33 .collect::<BTreeMap<_, _>>();
34
35 let mut outputs = TopologicalSort::<OutputFile>::new();
38 for (package_output, (_, package)) in &lookup_by_output {
39 match &package.source {
40 PackageSource::Local { .. }
41 | PackageSource::Prebuilt { .. }
42 | PackageSource::Manual => {
43 if !matches!(
47 package.output,
48 PackageOutput::Zone {
49 intermediate_only: true
50 }
51 ) {
52 outputs.insert(package_output.clone());
53 }
54 }
55 PackageSource::Composite { packages: deps } => {
56 for dep in deps {
57 outputs.add_dependency(OutputFile(dep.clone()), package_output.clone());
58 }
59 }
60 }
61 }
62
63 PackageDependencyIter {
64 lookup_by_output,
65 outputs,
66 }
67 }
68}
69
70pub struct PackageDependencyIter<'a> {
74 lookup_by_output: BTreeMap<OutputFile, (&'a PackageName, &'a Package)>,
75 outputs: TopologicalSort<OutputFile>,
76}
77
78impl<'a> Iterator for PackageDependencyIter<'a> {
79 type Item = Vec<(&'a PackageName, &'a Package)>;
80
81 fn next(&mut self) -> Option<Self::Item> {
82 if self.outputs.is_empty() {
83 return None;
84 }
85 let batch = self.outputs.pop_all();
86 assert!(
87 !batch.is_empty() || self.outputs.is_empty(),
88 "cyclic dependency in package manifest!"
89 );
90
91 Some(
92 batch
93 .into_iter()
94 .map(|output| {
95 *self.lookup_by_output.get(&output).unwrap_or_else(|| {
96 panic!("Could not find a package which creates '{}'", output.0)
97 })
98 })
99 .collect(),
100 )
101 }
102}
103
104#[derive(Clone, Deserialize, Serialize, Debug)]
106pub struct Config {
107 #[serde(default, rename = "package")]
109 pub packages: BTreeMap<PackageName, Package>,
110
111 #[serde(default)]
113 pub target: TargetConfig,
114}
115
116impl Config {
117 pub fn packages_to_build(&self, target: &TargetMap) -> PackageMap<'_> {
119 PackageMap(
120 self.packages
121 .iter()
122 .filter(|(_, pkg)| target.includes_package(pkg))
123 .collect(),
124 )
125 }
126
127 pub fn packages_to_deploy(&self, target: &TargetMap) -> PackageMap<'_> {
129 let all_packages = self.packages_to_build(target).0;
130 PackageMap(
131 all_packages
132 .into_iter()
133 .filter(|(_, pkg)| match pkg.output {
134 PackageOutput::Zone { intermediate_only } => !intermediate_only,
135 PackageOutput::Tarball => true,
136 })
137 .collect(),
138 )
139 }
140}
141
142#[derive(Clone, Deserialize, Serialize, Debug, Default)]
144pub struct TargetConfig {
145 #[serde(default, rename = "preset")]
147 pub presets: BTreeMap<PresetName, TargetMap>,
148}
149
150#[derive(Error, Debug)]
152pub enum ParseError {
153 #[error("Cannot parse toml: {0}")]
154 Toml(#[from] toml::de::Error),
155 #[error("IO error: {0}")]
156 Io(#[from] std::io::Error),
157}
158
159pub fn parse_manifest(manifest: &str) -> Result<Config, ParseError> {
161 let cfg = toml::from_str::<Config>(manifest)?;
162 Ok(cfg)
163}
164pub fn parse<P: AsRef<Path>>(path: P) -> Result<Config, ParseError> {
166 let contents = std::fs::read_to_string(path.as_ref())?;
167 parse_manifest(&contents)
168}
169
170#[cfg(test)]
171mod test {
172 use crate::config::ServiceName;
173
174 use super::*;
175
176 #[test]
177 fn test_order() {
178 let pkg_a_name = PackageName::new_const("pkg-a");
179 let pkg_a = Package {
180 service_name: ServiceName::new_const("a"),
181 source: PackageSource::Manual,
182 output: PackageOutput::Tarball,
183 only_for_targets: None,
184 setup_hint: None,
185 };
186
187 let pkg_b_name = PackageName::new_const("pkg-b");
188 let pkg_b = Package {
189 service_name: ServiceName::new_const("b"),
190 source: PackageSource::Composite {
191 packages: vec![pkg_a.get_output_file(&pkg_a_name)],
192 },
193 output: PackageOutput::Tarball,
194 only_for_targets: None,
195 setup_hint: None,
196 };
197
198 let cfg = Config {
199 packages: BTreeMap::from([
200 (pkg_a_name.clone(), pkg_a.clone()),
201 (pkg_b_name.clone(), pkg_b.clone()),
202 ]),
203 target: TargetConfig::default(),
204 };
205
206 let mut order = cfg.packages_to_build(&TargetMap::default()).build_order();
207 assert_eq!(order.next(), Some(vec![(&pkg_a_name, &pkg_a)]));
209 assert_eq!(order.next(), Some(vec![(&pkg_b_name, &pkg_b)]));
210 }
211
212 #[test]
217 #[should_panic(expected = "cyclic dependency in package manifest")]
218 fn test_cyclic_dependency() {
219 let pkg_a_name = PackageName::new_const("pkg-a");
220 let pkg_b_name = PackageName::new_const("pkg-b");
221 let pkg_a = Package {
222 service_name: ServiceName::new_const("a"),
223 source: PackageSource::Composite {
224 packages: vec![String::from("pkg-b.tar")],
225 },
226 output: PackageOutput::Tarball,
227 only_for_targets: None,
228 setup_hint: None,
229 };
230 let pkg_b = Package {
231 service_name: ServiceName::new_const("b"),
232 source: PackageSource::Composite {
233 packages: vec![String::from("pkg-a.tar")],
234 },
235 output: PackageOutput::Tarball,
236 only_for_targets: None,
237 setup_hint: None,
238 };
239
240 let cfg = Config {
241 packages: BTreeMap::from([
242 (pkg_a_name.clone(), pkg_a.clone()),
243 (pkg_b_name.clone(), pkg_b.clone()),
244 ]),
245 target: TargetConfig::default(),
246 };
247
248 let mut order = cfg.packages_to_build(&TargetMap::default()).build_order();
249 order.next();
250 }
251
252 #[test]
256 #[should_panic(expected = "Could not find a package which creates 'pkg-b.tar'")]
257 fn test_missing_dependency() {
258 let pkg_a_name = PackageName::new_const("pkg-a");
259 let pkg_a = Package {
260 service_name: ServiceName::new_const("a"),
261 source: PackageSource::Composite {
262 packages: vec![String::from("pkg-b.tar")],
263 },
264 output: PackageOutput::Tarball,
265 only_for_targets: None,
266 setup_hint: None,
267 };
268
269 let cfg = Config {
270 packages: BTreeMap::from([(pkg_a_name.clone(), pkg_a.clone())]),
271 target: TargetConfig::default(),
272 };
273
274 let mut order = cfg.packages_to_build(&TargetMap::default()).build_order();
275 order.next();
276 }
277}