rules.coffee | |
---|---|
rules models task pipelines using directed acyclic graph (dag.coffee) primitives. Following in the heritage of make, rules have a target, prerequisites, and a recipe. Unlike make target is a symbolic name and prerequisites can refer to other rules or to files on disk.
One way of describing their relationship would be: recipe(prerequisites) -> target Rules are modeled on the dag as FileNodes and RecipeNodes. The target is always a RecipeNode. It has inbound arcs coming from prerequisites which can be either RecipeNodes or FileNodes. If a recipe defines its outputs they are FileNodes and are connected with outbound arcs from the target RecipeNode. Rules with other rules as prerequisites will substitute the rule prereq with the rule's recipe's outputs if they exist. | |
Dependencies | { Graph, Node, Arc, NodeList } = require './dag'
_ = require 'underscore'
assert = require 'assert'
{ globSync } = require 'glob'
fs = require 'fs'
class RuleGraph extends Graph
refresh: (file) ->
do this.nodeMap[file].refresh
fileSources: (target) ->
this.subgraph(target)
.sources()
.ofType FileNode
recipeNodesTo: (target) ->
this.subgraph(target)
.topologicalOrdering()
.ofType RecipeNode
rule: (rule) ->
assert.ok rule instanceof Rule
graph = this
target = new RecipeNode rule.target, rule.recipe
graph.node target |
Glob Prereqs | rule.prereqs = _(rule.prereqs)
.chain()
.flatten()
.map( (prereq) ->
if /\*/.test prereq
globbed = globSync prereq
if globbed.length > 0
globbed
else
undefined
else
prereq
)
.compact()
.flatten()
.value()
rule.filePrereqs = []
rule.prereqs.forEach (prereq) -> |
Special prereqs for specifying a dependency on a task running or on the computed outputs of a task | specialPrereq = prereq.match /^(task|outputs)\((.*)\)$/
if specialPrereq
kind = specialPrereq[1]
prereq = specialPrereq[2] |
If a prereq already exists, we use it. Targets must therefore always be defined prior to being referenced as prerequisites in other rules. | input = graph.node prereq
if input and input instanceof RecipeNode
if specialPrereq == null
throw new Error "Task prerequisites must be referenced with task(#{prereq}) or outputs(#{prereq})"
if input not instanceof RecipeNode
console.error "Bad prereq: #{kind}(#{prereq}) - #{prereq} must be the name of a task"
process.exit 1
if kind == 'outputs'
inputsOutputs = graph.arcs.from(input).to().ofType(FileNode)
if not inputsOutputs.isEmpty()
inputsOutputs.forEach (inputsOutput) ->
graph.arc inputsOutput.name, target.name
rule.filePrereqs.push inputsOutput.name
return
else if specialPrereq
console.error "Error: task '#{target.name}' prereq '#{kind}(#{prereq})'-'#{prereq}' is not yet defined"
process.exit 1
else |
see if there's an expansion for it | input = new FileNode prereq
rule.filePrereqs.push prereq
graph.node input
graph.arc input.name, target.name
outputs = rule.recipe.outputs.call rule
outputs.forEach (output) ->
output = new FileNode output
graph.node output
graph.arc target.name, output.name
this |
Rule Graph Elements | class Rule
constructor: (@target, @prereqs, @recipe) ->
if _(@recipe).isFunction()
@recipe = new Recipe @recipe
class Recipe
constructor: (@recipe = (->), @outputs = (->[])) -> |
Additional Graph Nodes | class RecipeNode extends Node
constructor: (@name, @recipe) ->
equals: (node) ->
super(node) && node instanceof RecipeNode
clone: (node) -> new RecipeNode @name, @recipe
prereqs: (graph) -> graph.arcs.to(graph.node this.name).from().ofType FileNode
outputs: (graph) -> graph.arcs.from(graph.node this.name).to().ofType FileNode
refreshOutputs: (graph) ->
this.outputs(graph).forEach (fileNode) -> fileNode.refresh()
modifiedPrereqs: (graph) ->
prereqs = this.prereqs graph
outputs = this.outputs graph
if prereqs.isEmpty() or outputs.isEmpty() then return prereqs |
There are file inputs and outputs. If there is the same number of inputs as outputs we take a special case and compare them 1 by 1. If there are a different number of inputs and outputs we check to see if the greatest modified time of the inputs is greater than the oldest output. If so we should run. | if prereqs.count() == outputs.count()
return new NodeList _(_.zip prereqs.items,outputs.items)
.chain()
.map((pair) ->
[input,output] = pair
inputTime = input.mtime || Date.now()
outputTime = output.mtime || 0
if inputTime > outputTime
input
else
false
)
.compact()
.value()
else
prereqMax = _(prereqs.items).max (fileNode) -> fileNode.mtime ||= Date.now()
outputMin = _(outputs.items).min (fileNode) -> fileNode.mtime ||= 0
if prereqMax.mtime > outputMin.mtime
return prereqs
else
return new NodeList
shouldRun: (graph) ->
prereqs = this.prereqs graph |
Recipes without prereqs always run | if prereqs.isEmpty() then return true
outputs = this.outputs graph |
Recipes without file outputs always run | if outputs.isEmpty() then return true
return not this.modifiedPrereqs(graph).isEmpty()
run: (context, options) -> @recipe.recipe.call context, options
class FileNode extends Node
constructor: (@name) -> this.refresh()
equals: (node) ->
super(node) && node instanceof FileNode
clone: (node) -> new FileNode @name
refresh: ->
try
@stats = fs.statSync @name
@mtime = @stats.mtime
catch Error
@stats = {}
@mtime = undefined |
Exports | _(exports).extend { RuleGraph, Rule, RecipeNode, FileNode, Recipe }
|