use crate::package::{Package, PackageOutput, PackageSource};
use crate::target::Target;
use serde_derive::Deserialize;
use std::collections::BTreeMap;
use std::path::Path;
use thiserror::Error;
use topological_sort::TopologicalSort;
pub struct PackageMap<'a>(pub BTreeMap<&'a String, &'a Package>);
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct OutputFile(String);
impl<'a> PackageMap<'a> {
pub fn build_order(&self) -> PackageDependencyIter<'a> {
let lookup_by_output = self
.0
.iter()
.map(|(name, package)| (OutputFile(package.get_output_file(name)), (*name, *package)))
.collect::<BTreeMap<_, _>>();
let mut outputs = TopologicalSort::<OutputFile>::new();
for (package_output, (_, package)) in &lookup_by_output {
match &package.source {
PackageSource::Local { .. }
| PackageSource::Prebuilt { .. }
| PackageSource::Manual => {
if !matches!(
package.output,
PackageOutput::Zone {
intermediate_only: true
}
) {
outputs.insert(package_output.clone());
}
}
PackageSource::Composite { packages: deps } => {
for dep in deps {
outputs.add_dependency(OutputFile(dep.clone()), package_output.clone());
}
}
}
}
PackageDependencyIter {
lookup_by_output,
outputs,
}
}
}
pub struct PackageDependencyIter<'a> {
lookup_by_output: BTreeMap<OutputFile, (&'a String, &'a Package)>,
outputs: TopologicalSort<OutputFile>,
}
impl<'a> Iterator for PackageDependencyIter<'a> {
type Item = Vec<(&'a String, &'a Package)>;
fn next(&mut self) -> Option<Self::Item> {
if self.outputs.is_empty() {
return None;
}
let batch = self.outputs.pop_all();
assert!(
!batch.is_empty() || self.outputs.is_empty(),
"cyclic dependency in package manifest!"
);
Some(
batch
.into_iter()
.map(|output| {
*self.lookup_by_output.get(&output).unwrap_or_else(|| {
panic!("Could not find a package which creates '{}'", output.0)
})
})
.collect(),
)
}
}
#[derive(Deserialize, Debug)]
pub struct Config {
#[serde(default, rename = "package")]
pub packages: BTreeMap<String, Package>,
}
impl Config {
pub fn packages_to_build(&self, target: &Target) -> PackageMap<'_> {
PackageMap(
self.packages
.iter()
.filter(|(_, pkg)| target.includes_package(pkg))
.map(|(name, pkg)| (name, pkg))
.collect(),
)
}
pub fn packages_to_deploy(&self, target: &Target) -> PackageMap<'_> {
let all_packages = self.packages_to_build(target).0;
PackageMap(
all_packages
.into_iter()
.filter(|(_, pkg)| match pkg.output {
PackageOutput::Zone { intermediate_only } => !intermediate_only,
PackageOutput::Tarball => true,
})
.collect(),
)
}
}
#[derive(Error, Debug)]
pub enum ParseError {
#[error("Cannot parse toml: {0}")]
Toml(#[from] toml::de::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub fn parse_manifest(manifest: &str) -> Result<Config, ParseError> {
let cfg = toml::from_str::<Config>(manifest)?;
Ok(cfg)
}
pub fn parse<P: AsRef<Path>>(path: P) -> Result<Config, ParseError> {
let contents = std::fs::read_to_string(path.as_ref())?;
parse_manifest(&contents)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_order() {
let pkg_a_name = String::from("pkg-a");
let pkg_a = Package {
service_name: String::from("a"),
source: PackageSource::Manual,
output: PackageOutput::Tarball,
only_for_targets: None,
setup_hint: None,
};
let pkg_b_name = String::from("pkg-b");
let pkg_b = Package {
service_name: String::from("b"),
source: PackageSource::Composite {
packages: vec![pkg_a.get_output_file(&pkg_a_name)],
},
output: PackageOutput::Tarball,
only_for_targets: None,
setup_hint: None,
};
let cfg = Config {
packages: BTreeMap::from([
(pkg_a_name.clone(), pkg_a.clone()),
(pkg_b_name.clone(), pkg_b.clone()),
]),
};
let mut order = cfg.packages_to_build(&Target::default()).build_order();
assert_eq!(order.next(), Some(vec![(&pkg_a_name, &pkg_a)]));
assert_eq!(order.next(), Some(vec![(&pkg_b_name, &pkg_b)]));
}
#[test]
#[should_panic(expected = "cyclic dependency in package manifest")]
fn test_cyclic_dependency() {
let pkg_a_name = String::from("pkg-a");
let pkg_b_name = String::from("pkg-b");
let pkg_a = Package {
service_name: String::from("a"),
source: PackageSource::Composite {
packages: vec![String::from("pkg-b.tar")],
},
output: PackageOutput::Tarball,
only_for_targets: None,
setup_hint: None,
};
let pkg_b = Package {
service_name: String::from("b"),
source: PackageSource::Composite {
packages: vec![String::from("pkg-a.tar")],
},
output: PackageOutput::Tarball,
only_for_targets: None,
setup_hint: None,
};
let cfg = Config {
packages: BTreeMap::from([
(pkg_a_name.clone(), pkg_a.clone()),
(pkg_b_name.clone(), pkg_b.clone()),
]),
};
let mut order = cfg.packages_to_build(&Target::default()).build_order();
order.next();
}
#[test]
#[should_panic(expected = "Could not find a package which creates 'pkg-b.tar'")]
fn test_missing_dependency() {
let pkg_a_name = String::from("pkg-a");
let pkg_a = Package {
service_name: String::from("a"),
source: PackageSource::Composite {
packages: vec![String::from("pkg-b.tar")],
},
output: PackageOutput::Tarball,
only_for_targets: None,
setup_hint: None,
};
let cfg = Config {
packages: BTreeMap::from([(pkg_a_name.clone(), pkg_a.clone())]),
};
let mut order = cfg.packages_to_build(&Target::default()).build_order();
order.next();
}
}