mod error;
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use petgraph::graph::NodeIndex;
use petgraph::Graph;
#[cfg(feature = "petgraph_visible")]
pub use petgraph;
pub use crate::error::{DepResult, Error};
struct DependencyNode {
filename: PathBuf,
build_fn: Option<Box<dyn Fn(&Path, &[&Path]) -> Result<(), String>>>,
}
impl fmt::Debug for DependencyNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DependencyNode(\"{:?}\")", self.filename)
}
}
pub struct DepGraphBuilder {
edges: Vec<(
PathBuf,
Vec<PathBuf>,
Box<dyn Fn(&Path, &[&Path]) -> Result<(), String>>,
)>,
}
impl DepGraphBuilder {
pub fn new() -> DepGraphBuilder {
DepGraphBuilder { edges: Vec::new() }
}
pub fn add_rule<F, P1, P2>(
mut self,
filename: P1,
dependencies: &[P2],
build_fn: F,
) -> DepGraphBuilder
where
F: Fn(&Path, &[&Path]) -> Result<(), String> + 'static,
P1: AsRef<Path>,
P2: AsRef<Path>,
{
self.edges.push((
filename.as_ref().to_path_buf(),
dependencies
.iter()
.map(|s| s.as_ref().to_path_buf())
.collect(),
Box::new(build_fn),
));
self
}
pub fn add_dep_to_all<P>(mut self, dep: P) -> DepGraphBuilder
where
P: AsRef<Path>,
{
for edge in self.edges.iter_mut() {
edge.1.push(dep.as_ref().to_owned());
}
self
}
pub fn build(self) -> DepResult<DepGraph> {
let mut files = HashMap::new();
let mut edges_after_node = Vec::with_capacity(self.edges.len());
let mut graph = Graph::new();
for edge in self.edges.into_iter() {
let (filename, dependencies, build_fn) = edge;
if files.contains_key(&filename) {
return Err(Error::DuplicateFile);
}
let idx = graph.add_node(DependencyNode {
filename: filename.clone(),
build_fn: Some(build_fn),
});
files.insert(filename, idx);
edges_after_node.push((idx, dependencies));
}
for edge in edges_after_node.into_iter() {
let (idx, dependencies) = edge;
for dep in dependencies.into_iter() {
let maybe_dep = files.get(&dep).map(|v| *v);
if let Some(idx2) = maybe_dep {
graph.add_edge(idx, idx2, ());
} else {
let idx2 = graph.add_node(DependencyNode {
filename: dep.clone(),
build_fn: None,
});
files.insert(dep, idx2);
graph.add_edge(idx, idx2, ());
}
}
}
if petgraph::algo::is_cyclic_directed(&graph) {
return Err(Error::Cycle);
}
Ok(DepGraph {
graph: graph,
})
}
}
pub struct DepGraph {
graph: Graph<DependencyNode, ()>,
}
#[derive(Debug, Clone, Copy)]
pub enum MakeParams {
None,
ForceBuild,
}
impl DepGraph {
pub fn make(&self, make_params: MakeParams) -> DepResult<()> {
let ordered_deps_rev =
petgraph::algo::toposort(&self.graph, None).map_err(|_| Error::Cycle)?;
let force: bool = match make_params {
MakeParams::None => false,
MakeParams::ForceBuild => true,
};
for node in ordered_deps_rev.iter().rev() {
self.build_dependency(*node, force)?;
}
Ok(())
}
fn build_dependency(&self, idx: NodeIndex<u32>, force: bool) -> DepResult<()> {
let dep = self.graph.node_weight(idx).unwrap();
let children: Vec<&Path> = self
.graph
.neighbors_directed(idx, petgraph::Outgoing)
.map(|idx| self.graph.node_weight(idx).unwrap().filename.as_path())
.collect();
for child in children.iter() {
if !Path::new(child).exists() {
return Err(Error::MissingFile((*child).to_owned()));
}
}
if let Some(ref f) = dep.build_fn {
if force || dependencies_newer(&dep.filename, &children) {
f(&dep.filename, &children).map_err(|s| Error::BuildFailed(s))?;
}
}
if Path::new(&dep.filename).exists() {
Ok(())
} else {
Err(Error::MissingFile(dep.filename.clone()))
}
}
#[cfg(feature = "petgraph_visible")]
pub fn into_inner(self) -> (Graph<DependencyNode, ()>, HashMap<String, NodeIndex<u32>>) {
(self.graph, self.file_hash)
}
}
fn dependencies_newer(filename: &Path, deps: &[&Path]) -> bool {
if !filename.exists() {
return true;
}
let file_mod_time = fs::metadata(filename).unwrap().modified().unwrap();
for dep in deps {
let dep_mod_time = fs::metadata(Path::new(dep)).unwrap().modified().unwrap();
if dep_mod_time > file_mod_time {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io;
use std::io::{Read, Write};
use tempdir::TempDir;
fn copy_build(fname: &Path, deps: &[&Path]) -> Result<(), String> {
fn io_err_to_string(err: io::Error) -> String {
err.to_string()
}
let mut out = File::create(fname).map_err(io_err_to_string)?;
for d in deps {
let mut in_f = File::open(d).map_err(io_err_to_string)?;
let mut buf = String::new();
in_f.read_to_string(&mut buf).map_err(io_err_to_string)?;
write!(&mut out, "{}", buf).map_err(io_err_to_string)?;
}
Ok(())
}
#[test]
fn smoke_test() {
let tmp_dir = TempDir::new("depgraph-tests").unwrap();
let tmp = tmp_dir.path();
println!("tmp dir {:?}", tmp);
let makegraph = DepGraphBuilder::new()
.add_rule(
tmp.join("File1"),
&[tmp.join("file2"), tmp.join("file3")],
copy_build,
)
.add_rule(tmp.join("file2"), &[tmp.join("file3")], copy_build)
.add_rule(tmp.join("file4"), &[tmp.join("file5")], copy_build)
.add_dep_to_all(tmp.join("file6"))
.build()
.unwrap();
{
let mut file3 = File::create(tmp.join("file3")).unwrap();
write!(&mut file3, "file3\n").unwrap();
let mut file5 = File::create(tmp.join("file5")).unwrap();
write!(&mut file5, "file5\n").unwrap();
let mut file6 = File::create(tmp.join("file6")).unwrap();
write!(&mut file6, "file6\n").unwrap();
}
makegraph.make(MakeParams::None).unwrap();
}
}