maker 0.0.2

Generic Rusty declarative build system like GNU Make
Documentation
use std::cell::RefCell;
use std::rc::Rc;

type RecipeFn = dyn Fn(Target);
type PruneFn = dyn Fn(Target) -> bool;

struct TargetInner {
    name: String,
    dependencies: Vec<Target>,
    is_file: bool,
    prune_fn: Option<Box<PruneFn>>,
    recipe_fn: Option<Box<RecipeFn>>,
}

#[derive(Clone)]
pub struct Target {
    inner: Rc<RefCell<TargetInner>>,
}

fn modify_time(file: &str) -> Option<std::time::SystemTime> {
    std::path::Path::new(file).metadata().ok()?.modified().ok()
}

impl Target {
    /// Gets the name of the target.
    pub fn name(&self) -> String {
        self.inner.borrow().name.clone()
    }

    /// Gets the dependencies of the target.
    pub fn deps(&self) -> Vec<Target> {
        self.inner.borrow().dependencies.clone()
    }

    /// Creates a phony target with the given name.
    pub fn new_phony(name: &str) -> Self {
        Target {
            inner: Rc::new(RefCell::new(TargetInner {
                name: name.to_string(),
                prune_fn: None,
                is_file: false,
                dependencies: Vec::new(),
                recipe_fn: None,
            })),
        }
    }

    /// Creates a target that represents a file with the given name.
    pub fn new_file(name: &str) -> Self {
        Target {
            inner: Rc::new(RefCell::new(TargetInner {
                name: name.to_string(),
                prune_fn: None,
                is_file: true,
                dependencies: Vec::new(),
                recipe_fn: None,
            })),
        }
    }

    /// Adds a dependency to this target.
    pub fn depends_on_target(self, target: Target) -> Self {
        self.inner.borrow_mut().dependencies.push(target);
        self
    }

    /// Adds a file dependency to this target.
    ///
    /// Shorthand for `depends_on_target(Target::new_file(file))`.
    pub fn depends_on_file(self, file: &str) -> Self {
        let file_target = Target::new_file(file);
        self.depends_on_target(file_target)
    }

    /// Sets an early checker to determine if the target can be pruned.
    ///
    /// If the function returns `true`, the target and its dependencies are
    /// pruned, which means its dependencies will not make, and its recipe will
    /// not be executed.
    pub fn prune<F>(self, reached: F) -> Self
    where
        F: Fn(Target) -> bool + 'static,
    {
        self.inner.borrow_mut().prune_fn = Some(Box::new(reached) as Box<PruneFn>);
        self
    }

    /// Sets the recipe to make this target.
    pub fn recipe<F>(self, recipe: F) -> Self
    where
        F: Fn(Target) + 'static,
    {
        self.inner.borrow_mut().recipe_fn = Some(Box::new(recipe) as Box<RecipeFn>);
        self
    }

    /// Kicks off the make process for this target.
    ///
    /// Firstly it executes the `prune_fn` function, if it exists and returns
    /// `true`, the make process for this target stops. Otherwise then, the make
    /// processes of all dependencies are executed recursively. Then if this
    /// target is a file and all its dependencies are files and this file is
    /// newer than all its dependencies, the make recipe of this file is
    /// skipped. Finally, the recipe function is executed if it exists.
    pub fn make(&self) {
        if let Some(ref reached) = self.inner.borrow().prune_fn
            && reached(self.clone())
        {
            return;
        }
        for dep in self.deps() {
            dep.make();
        }
        if self.inner.borrow().is_file
            && let Some(modify) = modify_time(&self.name())
            && self.deps().iter().all(|dep| {
                dep.inner.borrow().is_file && {
                    let dep_modify = modify_time(&dep.name());
                    dep_modify.is_some() && dep_modify.unwrap() < modify
                }
            })
        {
            return;
        }
        if let Some(ref recipe) = self.inner.borrow().recipe_fn {
            recipe(self.clone());
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::{
        fs::File,
        io::{Read, Write},
    };

    #[test]
    fn test_make_files() {
        let dir = std::env::args().nth(0).unwrap();
        let dir = std::path::Path::new(dir.as_str()).parent().unwrap();
        let src1 = dir.join("src1").to_str().unwrap().to_owned();
        let src2 = dir.join("src2").to_str().unwrap().to_owned();
        let src3 = dir.join("src3").to_str().unwrap().to_owned();
        let dst = dir.join("dst").to_str().unwrap().to_owned();

        let mut fopt = File::options();
        fopt.write(true).create(true);

        let mut s1 = fopt.open(&src1).unwrap();
        s1.write(b"src1").unwrap();
        drop(s1);
        let mut s2 = fopt.open(&src2).unwrap();
        s2.write(b"src2").unwrap();
        drop(s2);
        let fopt1 = fopt.clone();
        let fopt2 = fopt.clone();

        let t3 = Target::new_file(&src3)
            .depends_on_file(&src2)
            .recipe(move |t| {
                let mut s3 = fopt1.open(t.name()).unwrap();
                let src2 = t.deps().remove(0);
                let mut s2 = File::open(src2.name()).unwrap();
                let mut buf = Vec::new();
                s2.read_to_end(&mut buf).unwrap();
                let buf = buf.repeat(3);
                s3.write_all(&buf).unwrap();
            });
        let d = Target::new_file(&dst)
            .depends_on_file(&src1)
            .depends_on_target(t3)
            .recipe(move |t| {
                let mut d = fopt2.open(t.name()).unwrap();
                let mut buf = Vec::new();
                for dep in t.deps() {
                    let mut s = File::open(dep.name()).unwrap();
                    let mut buf1 = Vec::new();
                    s.read_to_end(&mut buf1).unwrap();
                    buf.append(&mut buf1);
                }
                d.write_all(&buf).unwrap();
            });
        d.make();
        let mut buf = Vec::new();
        let mut f = File::open(&dst).unwrap();
        f.read_to_end(&mut buf).unwrap();
        assert_eq!(buf, b"src1src2src2src2");
    }
}