1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
//! A library to help build dependencies externally from rust. Uses petgraph under the hood for
//! managing the graph structure.
//!
//! # Example
//! *An example is worth a thousand words* - made up quote.
//!
//! ## Example build script
//!
//! ```no_run
//! extern crate depgraph;
//! use std::path::Path;
//! use std::{fs, env};
//! use std::process::Command;
//!
//! fn build_assembly(out: &Path, deps: &[&Path]) -> Result<(), String> {
//!     // Make sure the folder we're going to output to exists.
//!     let out_dir = out.parent().unwrap();
//!     fs::create_dir_all(out_dir).unwrap();
//!
//!     // Run the command with correct argument order
//!     Command::new("yasm").args(&["-f", "elf64", "-o"]).arg(out).args(deps)
//!         .status().unwrap();
//!     // Everything went ok so we return Ok(()). Instead of panicking, we could
//!     // have returned an error message and handled it in main.
//!     Ok(())
//! }
//!
//! fn main() {
//!     // Get the directory we should put files in.
//!     let out_dir = env::var("OUT_DIR").unwrap();
//!     let out_dir = Path::new(&out_dir);
//!     // Create the graph builder
//!     let graph = depgraph::DepGraphBuilder::new()
//!     // Add a rule to build an object file from an asm file using the build
//!     // script in `build_assembly`.
//!       .add_rule(out_dir.join("out/path/file.o"),
//!                 &[Path::new("src/input_file.asm")],
//!                 build_assembly)
//!     // Build the graph, internally this checks for cyclic dependencies.
//!       .build().unwrap();
//!     // Run the necessary build scripts in the correct order.
//!     graph.make(depgraph::MakeParams::None).unwrap();
//! }
//! ```
//!


extern crate petgraph;
#[cfg(test)]
extern crate tempdir;

mod error;

use std::fs;
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use std::fmt;

use petgraph::Graph;
use petgraph::graph::NodeIndex;

#[cfg(feature = "petgraph_visible")]
pub use petgraph;

pub use error::{Error, DepResult};

/// (Internal) Information on a dependency (how to build it and what it's called)
///
/// TODO keep copy of dependencies in order, so we don't have to look them up on the graph, and
/// they stay in order
struct DependencyNode {
    filename: PathBuf,
    build_fn: Option<Box<Fn(&Path, &[&Path]) -> Result<(), String>>>,
}

impl fmt::Debug for DependencyNode {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "DependencyNode(\"{:?}\")", self.filename)
    }
}

/// Used to construct a DepGraph
///
/// See the module level documentation for an example of how to use this
pub struct DepGraphBuilder {
    /// List of edges, .0 is dependent, .1 is dependencies, .2 is build fn
    edges: Vec<(PathBuf, Vec<PathBuf>, Box<Fn(&Path, &[&Path]) -> Result<(), String>>)>,
}

impl DepGraphBuilder {
    /// Create a `DepGraphBuilder` with no rules.
    pub fn new() -> DepGraphBuilder {
        DepGraphBuilder { edges: Vec::new() }
    }

    /// Add a new rule (a file with its dependent files and build instructions).
    ///
    /// These can be added in any order, and can be chained.
    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
    }

    /// Add a dependency to all previously added files. Will only affect previously added files,
    /// not those added in the future.
    ///
    /// This can be used to make all rules depend on `build.rs`, for example.
    pub fn add_dep_to_all<P>(mut self, dep: P) -> DepGraphBuilder
        where P: AsRef<Path>
    {
        for mut edge in self.edges.iter_mut() {
            edge.1.push(dep.as_ref().to_owned());
        }
        self
    }

    /// Build the make graph and check for errors like cyclic dependencies and duplicate files.
    pub fn build(self) -> DepResult<DepGraph> {
        // used to check a file isn't added more than once. (filename -> NodeId)
        let mut files = HashMap::new();
        // used between passes to store edges
        let mut edges_after_node = Vec::with_capacity(self.edges.len());
        // the resulting graph
        let mut graph = Graph::new();

        // Job of first iteration is to add nodes and save ids for them
        for edge in self.edges.into_iter() {
            let (filename, dependencies, build_fn) = edge;
            // error if file already added
            if files.contains_key(&filename) {
                return Err(Error::DuplicateFile);
            }
            // add node to graph and get index
            let idx = graph.add_node(DependencyNode {
                filename: filename.clone(),
                build_fn: Some(build_fn),
            });
            // add file to list
            files.insert(filename, idx);
            edges_after_node.push((idx, dependencies));
        }

        // Job of second iteration is to add in edges using `edges_after_node` and add in leaves
        // for files not found elsewhere
        for edge in edges_after_node.into_iter() {
            let (idx, dependencies) = edge;
            for dep in dependencies.into_iter() {
                // value is just number so deref to copy it
                let maybe_dep = files.get(&dep).map(|v| *v);
                if let Some(idx2) = maybe_dep {
                    // file already a dependency, so add directed edge from file to it's dependency
                    graph.add_edge(idx, idx2, ());
                } else {
                    // file not yet a dependency - add it
                    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,
            //file_hash: files,
        })
    }
}

/// Contains the checked and parsed dependency graph, ready for execution (`fn make`)
pub struct DepGraph {
    /// Node is file (weight is filename, build function), edge is dependency
    graph: Graph<DependencyNode, ()>,
    //file_hash: HashMap<String, NodeIndex<u32>>,
}

/// When running the build scripts, we can either only build when output files are newer than their
/// dependencies, or we can force the build script to run regardless. This enum allows for those
/// two choices.
#[derive(Debug, Clone, Copy)]
pub enum MakeParams {
    /// Just build normally, where we only rebuild if the source was updated
    None,
    /// Always build, regardless of status of source
    ForceBuild,
}

impl DepGraph {
    /// Run the build
    ///
    /// If force is true, all build functions will be run, regardless of file times, otherwise
    /// build will only be run if one of the dependency files is newer than the output file.
    // There are possible optimizations here as there are redundent metadata checks, I don't think
    // this is a big deal though.
    pub fn make(&self, make_params: MakeParams) -> DepResult<()> {
        // Get files in dependency order
        // Needs to be reversed to build in right order
        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(())
    }

    /// Helper function to build a specific dependency
    fn build_dependency(&self, idx: NodeIndex<u32>, force: bool) -> DepResult<()> {
        let dep = self.graph.node_weight(idx).unwrap();
        // collect names of children (don't copy strings)
        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 there is a build script, and dependency timestamps are newer, run it
        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),
                )?;
            }
        }
        // check that file has been created
        if Path::new(&dep.filename).exists() {
            Ok(())
        } else {
            Err(Error::MissingFile(dep.filename.clone()))
        }
        //println!("{:?}", children);
    }

    /// Get the underlying graph
    #[cfg(feature = "petgraph_visible")]
    pub fn into_inner(self) -> (Graph<DependencyNode, ()>, HashMap<String, NodeIndex<u32>>) {
        (self.graph, self.file_hash)
    }
}

/// Checks if any of the files in the dependency list are newer than the file given by `filename`.
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 std::io::{Write, Read};
    use std::fs::File;
    use std::io;
    use super::*;
    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();
    }
}