Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Introducing new way to reference other rules as prerequisites and exp…

…licitely depend on either the task running or on the outputs of the tasks.
  • Loading branch information...
commit b2f3fc573a4ead911ccfc476b6f5875907a8e1fe 1 parent 3018256
@KrisJordan authored
View
19 Cakefile
@@ -1,28 +1,37 @@
require './src/icing'
+_ = require 'underscore'
option '-s','--spec','Run vows with spec option'
task 'version', -> console.log '0.1'
-task 'test', ['spec/*'], (options) ->
+task 'test', ['spec/*','src/*'], (options) ->
args = []
if options.spec?
args.push '--spec'
- command = "vows #{args.join ' '} #{this.prereqs.join(' ')}"
+ tests = _(this.modifiedPrereqs).filter (prereq) -> prereq.match /^spec/
+ src = _(this.modifiedPrereqs).filter (prereq) -> prereq.match /^src/
+ command = "vows #{args.join ' '} #{tests.join(' ')}"
this.exec [
command
]
+task 'compile', ['task(test)','src/*'],
+ exec: -> this.exec "coffee -c -o lib/ #{this.modifiedPrereqs.join(' ')}"
+ outputs: ->
+ for prereq in this.filePrereqs
+ prereq.replace /src\/(.*).coffee/,"lib/$1.js"
+
task 'docs', 'Generate docco documentation', ['src/*'],
exec: (options) ->
this.exec "docco #{this.modifiedPrereqs.join(' ')}"
outputs: ->
- for prereq in this.prereqs
+ for prereq in this.filePrereqs
prereq.replace /src\/(.*).coffee/,"docs/$1.html"
-task 'all', 'Test and Document', ['test','docs'], (options) -> this.finished()
+task 'all', 'Test and Document', ['task(compile)','task(docs)'], (options) -> this.finished()
task 'clean', 'Remove Generated Files', [], ->
this.exec [
- "rm -rf docs/*"
+ "rm -rf docs/* lib/*"
]
View
3  README.markdown
@@ -0,0 +1,3 @@
+# icing
+
+icing adds dependency management to Cakefiles
View
2  docs/dag.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html> <html> <head> <title>dag.coffee</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <link rel="stylesheet" media="all" href="docco.css" /> </head> <body> <div id="container"> <div id="background"></div> <div id="jump_to"> Jump To &hellip; <div id="jump_wrapper"> <div id="jump_page"> <a class="source" href="dag.html"> dag.coffee </a> <a class="source" href="icing.html"> icing.coffee </a> </div> </div> </div> <table cellpadding="0" cellspacing="0"> <thead> <tr> <th class="docs"> <h1> dag.coffee </h1> </th> <th class="code"> </th> </tr> </thead> <tbody> <tr id="section-1"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-1">&#182;</a> </div> <p><strong>dag</strong> is a simple library for working with
+<!DOCTYPE html> <html> <head> <title>dag.coffee</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <link rel="stylesheet" media="all" href="docco.css" /> </head> <body> <div id="container"> <div id="background"></div> <div id="jump_to"> Jump To &hellip; <div id="jump_wrapper"> <div id="jump_page"> <a class="source" href="dag.html"> dag.coffee </a> <a class="source" href="icing.html"> icing.coffee </a> <a class="source" href="rules.html"> rules.coffee </a> </div> </div> </div> <table cellpadding="0" cellspacing="0"> <thead> <tr> <th class="docs"> <h1> dag.coffee </h1> </th> <th class="code"> </th> </tr> </thead> <tbody> <tr id="section-1"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-1">&#182;</a> </div> <p><strong>dag</strong> is a simple library for working with
<a href="http://en.wikipedia.org/wiki/Directed_acyclic_graph">directed acyclic graphs</a>.
Its primary purpose is to support command execution pipelines for <strong>icing</strong>.</p>
View
17 docs/icing.html
@@ -49,7 +49,13 @@
task 'A', 'run A', ['B'], -&gt; console.log 'A'
</code></pre>
-<p>Recipes' <em>this</em> context provides special </p> </td> <td class="code"> <div class="highlight"><pre></pre></div> </td> </tr> <tr id="section-2"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-2">&#182;</a> </div> <h3>Options</h3> </td> <td class="code"> <div class="highlight"><pre><span class="nx">option</span> <span class="s1">&#39;-v&#39;</span><span class="p">,</span> <span class="s1">&#39;--verbose&#39;</span><span class="p">,</span> <span class="s1">&#39;Display progress as tasks are executed&#39;</span>
+<p>Recipes' <em>this</em> context provides helper functionality.</p>
+
+<p>TODO: Document recipe this context
+TODO: In watch mode run from modified source rather than beginning
+TODO: Watch mode with globbed prereqs should watch for globbed changes and rebuild
+ graph.
+TODO: Test with non-existant inputs.</p> </td> <td class="code"> <div class="highlight"><pre></pre></div> </td> </tr> <tr id="section-2"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-2">&#182;</a> </div> <h3>Options</h3> </td> <td class="code"> <div class="highlight"><pre><span class="nx">option</span> <span class="s1">&#39;-v&#39;</span><span class="p">,</span> <span class="s1">&#39;--verbose&#39;</span><span class="p">,</span> <span class="s1">&#39;Display progress as tasks are executed&#39;</span>
<span class="nx">option</span> <span class="s1">&#39;-w&#39;</span><span class="p">,</span> <span class="s1">&#39;--watch&#39;</span><span class="p">,</span> <span class="s1">&#39;Monitor files for changes and automatically rebuild&#39;</span></pre></div> </td> </tr> <tr id="section-3"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-3">&#182;</a> </div> <h3>Dependencies and Globals</h3> </td> <td class="code"> <div class="highlight"><pre><span class="p">{</span> <span class="nx">RuleGraph</span><span class="p">,</span> <span class="nx">Rule</span><span class="p">,</span> <span class="nx">RecipeNode</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span> <span class="s1">&#39;./rules&#39;</span>
<span class="p">{</span> <span class="nx">exec</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span> <span class="s1">&#39;child_process&#39;</span>
<span class="nv">fs = </span><span class="nx">require</span> <span class="s1">&#39;fs&#39;</span></pre></div> </td> </tr> <tr id="section-4"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-4">&#182;</a> </div> <p>Preserve a reference to cake's task, we'll be using it.</p> </td> <td class="code"> <div class="highlight"><pre><span class="nv">cakeTask = </span><span class="nx">global</span><span class="p">.</span><span class="nx">task</span>
@@ -75,7 +81,6 @@
<span class="nv">allRecipesProcessed = </span><span class="kc">false</span>
<span class="nv">recipeNode = </span><span class="p">{}</span>
-
<span class="nv">runTask = </span><span class="o">-&gt;</span>
<span class="nv">taskIsRunning = </span><span class="kc">true</span>
<span class="nv">recipeNodes = </span><span class="nx">graph</span><span class="p">.</span><span class="nx">recipeNodesTo</span> <span class="nx">target</span>
@@ -105,14 +110,16 @@
<span class="nx">do</span> <span class="nx">runNextRecipeCallback</span>
<span class="k">else</span>
<span class="nv">allRecipesProcessed = </span><span class="kc">true</span>
- <span class="k">if</span> <span class="o">not</span> <span class="nx">aRecipeRan</span> <span class="o">and</span> <span class="nx">options</span><span class="p">.</span><span class="nx">verbose</span><span class="o">?</span></pre></div> </td> </tr> <tr id="section-8"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-8">&#182;</a> </div> <p>Homage</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span> <span class="s2">&quot;cake: Nothing to be done for `#{target}&#39;.&quot;</span>
+ <span class="k">if</span> <span class="o">not</span> <span class="nx">aRecipeRan</span> <span class="o">and</span> <span class="nx">options</span><span class="p">.</span><span class="nx">verbose</span><span class="o">?</span></pre></div> </td> </tr> <tr id="section-8"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-8">&#182;</a> </div> <p>Homage</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span> <span class="nx">stylize</span> <span class="s2">&quot;cake: Nothing to be done for `#{target}&#39;.&quot;</span><span class="p">,</span> <span class="s1">&#39;yellow&#39;</span>
+ <span class="k">if</span> <span class="nx">options</span><span class="p">.</span><span class="nx">watch</span><span class="o">?</span>
+ <span class="nx">console</span><span class="p">.</span><span class="nx">log</span> <span class="nx">stylize</span> <span class="s2">&quot;task &#39;#{target}&#39; finished&quot;</span><span class="p">,</span><span class="s1">&#39;grey&#39;</span>
<span class="nv">taskIsRunning = </span><span class="kc">false</span>
<span class="nx">do</span> <span class="nx">runNextRecipeCallback</span>
<span class="k">if</span> <span class="nx">options</span><span class="p">.</span><span class="nx">watch</span><span class="o">?</span>
<span class="nv">fileSources = </span><span class="nx">graph</span><span class="p">.</span><span class="nx">fileSources</span><span class="p">(</span><span class="nx">target</span><span class="p">).</span><span class="nx">names</span><span class="p">()</span>
<span class="k">if</span> <span class="nx">fileSources</span><span class="p">.</span><span class="nx">length</span> <span class="o">==</span> <span class="mi">0</span>
- <span class="nx">console</span><span class="p">.</span><span class="nx">error</span> <span class="nx">stylize</span> <span class="s2">&quot;Nothing to watch.&quot;</span><span class="p">,</span> <span class="s1">&#39;red&#39;</span>
+ <span class="nx">console</span><span class="p">.</span><span class="nx">error</span> <span class="nx">stylize</span> <span class="s2">&quot;cake: Nothing to watch for `#{target}&#39;&quot;</span><span class="p">,</span> <span class="s1">&#39;yellow&#39;</span>
<span class="nx">fileSources</span><span class="p">.</span><span class="nx">forEach</span> <span class="nf">(file) -&gt;</span>
<span class="nx">fs</span><span class="p">.</span><span class="nx">watchFile</span> <span class="nx">file</span><span class="p">,</span> <span class="p">{</span><span class="nx">interval</span><span class="o">:</span><span class="mi">250</span><span class="p">},</span> <span class="nf">(curr,prev) -&gt;</span>
@@ -150,7 +157,7 @@
<span class="k">if</span> <span class="o">not</span> <span class="nx">error</span><span class="o">?</span>
<span class="nx">runNextCommandCallback</span><span class="p">()</span>
<span class="k">else</span>
- <span class="nx">failedFn</span><span class="p">()</span>
+ <span class="nx">failedFn</span><span class="p">(</span><span class="nx">stderr</span><span class="p">)</span>
<span class="k">else</span>
<span class="nx">finishedFn</span><span class="p">()</span>
View
26 docs/rules.html
@@ -56,19 +56,35 @@
<span class="p">.</span><span class="nx">flatten</span><span class="p">()</span>
<span class="p">.</span><span class="nx">value</span><span class="p">()</span>
- <span class="nx">rule</span><span class="p">.</span><span class="nx">prereqs</span><span class="p">.</span><span class="nx">forEach</span> <span class="nf">(prereq) -&gt;</span></pre></div> </td> </tr> <tr id="section-4"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-4">&#182;</a> </div> <p>If a prereq already exists, we use it. Targets must therefore
+ <span class="nv">rule.filePrereqs = </span><span class="p">[]</span>
+
+ <span class="nx">rule</span><span class="p">.</span><span class="nx">prereqs</span><span class="p">.</span><span class="nx">forEach</span> <span class="nf">(prereq) -&gt;</span></pre></div> </td> </tr> <tr id="section-4"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-4">&#182;</a> </div> <p>Special prereqs for specifying a dependency on a task running
+or on the computed outputs of a task</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">specialPrereq = </span><span class="nx">prereq</span><span class="p">.</span><span class="nx">match</span> <span class="sr">/^(task|outputs)\((.*)\)$/</span>
+ <span class="k">if</span> <span class="nx">specialPrereq</span>
+ <span class="nv">kind = </span><span class="nx">specialPrereq</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
+ <span class="nv">prereq = </span><span class="nx">specialPrereq</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span></pre></div> </td> </tr> <tr id="section-5"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-5">&#182;</a> </div> <p>If a prereq already exists, we use it. Targets must therefore
always be defined prior to being referenced as prerequisites in other
-rules.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="k">if</span> <span class="nx">graph</span><span class="p">.</span><span class="nx">node</span> <span class="nx">prereq</span>
- <span class="nv">input = </span><span class="nx">graph</span><span class="p">.</span><span class="nx">node</span> <span class="nx">prereq</span></pre></div> </td> </tr> <tr id="section-5"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-5">&#182;</a> </div> <p>There's a special case when a RecipeNode's recipe has outputs. When
-other RecipeNode targets have one as a prereq its dependency is on
-those FileNode outputs and not the RecipeNode itself.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="k">if</span> <span class="nx">input</span> <span class="k">instanceof</span> <span class="nx">RecipeNode</span>
+rules.</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">input = </span><span class="nx">graph</span><span class="p">.</span><span class="nx">node</span> <span class="nx">prereq</span>
+ <span class="k">if</span> <span class="nx">input</span> <span class="o">and</span> <span class="nx">input</span> <span class="k">instanceof</span> <span class="nx">RecipeNode</span>
+ <span class="k">if</span> <span class="nx">specialPrereq</span> <span class="o">==</span> <span class="kc">null</span>
+ <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span> <span class="s2">&quot;Task prerequisites must be referenced with task(#{prereq}) or outputs(#{prereq})&quot;</span>
+ <span class="k">if</span> <span class="nx">input</span> <span class="o">not</span> <span class="k">instanceof</span> <span class="nx">RecipeNode</span>
+ <span class="nx">console</span><span class="p">.</span><span class="nx">error</span> <span class="s2">&quot;Bad prereq: #{kind}(#{prereq}) - #{prereq} must be the name of a task&quot;</span>
+ <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span> <span class="mi">1</span>
+ <span class="k">if</span> <span class="nx">kind</span> <span class="o">==</span> <span class="s1">&#39;outputs&#39;</span>
<span class="nv">inputsOutputs = </span> <span class="nx">graph</span><span class="p">.</span><span class="nx">arcs</span><span class="p">.</span><span class="nx">from</span><span class="p">(</span><span class="nx">input</span><span class="p">).</span><span class="nx">to</span><span class="p">().</span><span class="nx">ofType</span><span class="p">(</span><span class="nx">FileNode</span><span class="p">)</span>
<span class="k">if</span> <span class="o">not</span> <span class="nx">inputsOutputs</span><span class="p">.</span><span class="nx">isEmpty</span><span class="p">()</span>
<span class="nx">inputsOutputs</span><span class="p">.</span><span class="nx">forEach</span> <span class="nf">(inputsOutput) -&gt;</span>
<span class="nx">graph</span><span class="p">.</span><span class="nx">arc</span> <span class="nx">inputsOutput</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="nx">target</span><span class="p">.</span><span class="nx">name</span>
+ <span class="nx">rule</span><span class="p">.</span><span class="nx">filePrereqs</span><span class="p">.</span><span class="nx">push</span> <span class="nx">inputsOutput</span><span class="p">.</span><span class="nx">name</span>
<span class="k">return</span>
+ <span class="k">else</span> <span class="k">if</span> <span class="nx">specialPrereq</span>
+ <span class="nx">console</span><span class="p">.</span><span class="nx">error</span> <span class="s2">&quot;Error: task &#39;#{target.name}&#39; prereq &#39;#{kind}(#{prereq})&#39;-&#39;#{prereq}&#39; is not yet defined&quot;</span>
+ <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span> <span class="mi">1</span>
<span class="k">else</span></pre></div> </td> </tr> <tr id="section-6"> <td class="docs"> <div class="pilwrap"> <a class="pilcrow" href="#section-6">&#182;</a> </div> <p>see if there's an expansion for it</p> </td> <td class="code"> <div class="highlight"><pre> <span class="nv">input = </span><span class="k">new</span> <span class="nx">FileNode</span> <span class="nx">prereq</span>
+ <span class="nx">rule</span><span class="p">.</span><span class="nx">filePrereqs</span><span class="p">.</span><span class="nx">push</span> <span class="nx">prereq</span>
<span class="nx">graph</span><span class="p">.</span><span class="nx">node</span> <span class="nx">input</span>
+
<span class="nx">graph</span><span class="p">.</span><span class="nx">arc</span> <span class="nx">input</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="nx">target</span><span class="p">.</span><span class="nx">name</span>
<span class="nv">outputs = </span><span class="nx">rule</span><span class="p">.</span><span class="nx">recipe</span><span class="p">.</span><span class="nx">outputs</span><span class="p">.</span><span class="nx">call</span> <span class="nx">rule</span>
View
252 lib/dag.js
@@ -0,0 +1,252 @@
+(function() {
+ var Arc, ArcList, Graph, List, Node, NodeList, _;
+ var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
+ for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
+ function ctor() { this.constructor = child; }
+ ctor.prototype = parent.prototype;
+ child.prototype = new ctor;
+ child.__super__ = parent.prototype;
+ return child;
+ };
+ _ = require('underscore');
+ Graph = (function() {
+ function Graph(nodes, arcs) {
+ var graph;
+ this.nodes = nodes != null ? nodes : new NodeList;
+ this.arcs = arcs != null ? arcs : new ArcList;
+ this.nodeMap = {};
+ this.arcMap = {};
+ graph = this;
+ this.nodes.forEach(function(node) {
+ return graph.node(node);
+ });
+ }
+ Graph.prototype.node = function(node) {
+ if (_(node).isString()) {
+ if (this.nodeMap[node] != null) {
+ return this.nodeMap[node];
+ } else {
+ return void 0;
+ }
+ }
+ if (this.nodeMap[node.name] != null) {
+ if (node.equals(this.nodeMap[node.name])) {
+ return this;
+ } else {
+ throw new Error("Node of name " + node.name + " already exists.");
+ }
+ }
+ this.nodes.push(node);
+ this.nodeMap[node.name] = node;
+ return this;
+ };
+ Graph.prototype.arc = function(fromName, toName) {
+ var arc, fromNode, toNode;
+ if (this.arcMap[fromName] != null) {
+ if (this.arcMap[fromName][toName] != null) {
+ return this;
+ }
+ }
+ if (!(this.nodeMap[fromName] != null)) {
+ throw new Error("Node " + fromName + " does not exist");
+ } else {
+ fromNode = this.nodeMap[fromName];
+ }
+ if (!(this.nodeMap[toName] != null)) {
+ throw new Error("Node " + toName + " does not exist");
+ } else {
+ toNode = this.nodeMap[toName];
+ }
+ arc = new Arc(fromNode, toNode);
+ this.arcs.push(arc);
+ if (!(this.arcMap[fromNode.name] != null)) {
+ this.arcMap[fromNode.name] = {};
+ }
+ this.arcMap[fromNode.name][toNode.name] = arc;
+ return this;
+ };
+ Graph.prototype.sources = function() {
+ var nodesWithoutInboundArcs;
+ nodesWithoutInboundArcs = this.nodes.clone();
+ this.arcs.to().forEach(function(node) {
+ return nodesWithoutInboundArcs.remove(node);
+ });
+ return nodesWithoutInboundArcs;
+ };
+ Graph.prototype.topologicalOrdering = function() {
+ var arcs, nodes, source, sources;
+ nodes = new NodeList;
+ sources = this.sources();
+ arcs = this.arcs.clone();
+ while (!sources.isEmpty()) {
+ source = sources.pop();
+ nodes.push(source);
+ arcs.from(source).forEach(function(arc) {
+ arcs.remove(arc);
+ if (arcs.to(arc.to).isEmpty()) {
+ return sources.push(arc.to);
+ }
+ });
+ }
+ if (arcs.isEmpty()) {
+ return nodes;
+ } else {
+ throw new Error("Cycle detected in graph.");
+ }
+ };
+ Graph.prototype.hasCycle = function() {
+ try {
+ this.topologicalOrdering();
+ return false;
+ } catch (Error) {
+ return true;
+ }
+ };
+ Graph.prototype.subgraph = function(target) {
+ var graph, reconstruct, subgraph;
+ if (this.hasCycle()) {
+ throw new Error("Can't create subgraph in a cyclic graph.");
+ }
+ if (!(this.nodeMap[target] != null)) {
+ throw new Error("Target " + target + " does not exist.");
+ } else {
+ target = this.nodeMap[target];
+ }
+ graph = this;
+ subgraph = new Graph;
+ reconstruct = function(to) {
+ var toClone;
+ toClone = to.clone();
+ subgraph.node(toClone);
+ return graph.arcs.to(to).forEach(function(arc) {
+ var fromClone;
+ fromClone = arc.from.clone();
+ subgraph.node(fromClone);
+ subgraph.arc(fromClone.name, toClone.name);
+ return reconstruct(arc.from);
+ });
+ };
+ reconstruct(target);
+ return subgraph;
+ };
+ return Graph;
+ })();
+ Node = (function() {
+ function Node(name) {
+ this.name = name;
+ }
+ Node.prototype.clone = function() {
+ return new Node(this.name);
+ };
+ Node.prototype.equals = function(node) {
+ return this.name === node.name;
+ };
+ return Node;
+ })();
+ Arc = (function() {
+ function Arc(from, to) {
+ this.from = from;
+ this.to = to;
+ if (this.from === this.to) {
+ throw new Error("An arc's from Node cannot also be its to Node");
+ }
+ }
+ return Arc;
+ })();
+ List = (function() {
+ function List(items) {
+ this.items = items != null ? items : [];
+ }
+ List.prototype.push = function(item) {
+ if (_(this.items).contains(item)) {
+ this.items = _(this.items).without(item);
+ }
+ return this.items.push(item);
+ };
+ List.prototype.filter = function(fn) {
+ return _(this.items).filter(fn);
+ };
+ List.prototype.clone = function() {
+ return new List(this.items.slice(0));
+ };
+ List.prototype.forEach = function(fn) {
+ return _(this.items).forEach(fn);
+ };
+ List.prototype.remove = function(item) {
+ return this.items = _(this.items).without(item);
+ };
+ List.prototype.isEmpty = function() {
+ return this.items.length === 0;
+ };
+ List.prototype.pop = function() {
+ return this.items.pop();
+ };
+ List.prototype.shift = function() {
+ return this.items.shift();
+ };
+ List.prototype.count = function() {
+ return this.items.length;
+ };
+ List.prototype.pluck = function(property) {
+ return _(this.items).pluck(property);
+ };
+ return List;
+ })();
+ NodeList = (function() {
+ function NodeList() {
+ NodeList.__super__.constructor.apply(this, arguments);
+ }
+ __extends(NodeList, List);
+ NodeList.prototype.ofType = function(type) {
+ return new NodeList(_(this.items).filter(function(node) {
+ return node instanceof type;
+ }));
+ };
+ NodeList.prototype.clone = function() {
+ return new NodeList(NodeList.__super__.clone.call(this).items);
+ };
+ NodeList.prototype.names = function() {
+ return _(this.items).pluck('name');
+ };
+ return NodeList;
+ })();
+ ArcList = (function() {
+ function ArcList() {
+ ArcList.__super__.constructor.apply(this, arguments);
+ }
+ __extends(ArcList, List);
+ ArcList.prototype.from = function(node) {
+ if (node != null) {
+ return new ArcList(_(this.items).filter(function(arc) {
+ return arc.from === node;
+ }));
+ } else {
+ return new NodeList(this.pluckUniq('from'));
+ }
+ };
+ ArcList.prototype.to = function(node) {
+ if (node != null) {
+ return new ArcList(_(this.items).filter(function(arc) {
+ return arc.to === node;
+ }));
+ } else {
+ return new NodeList(this.pluckUniq('to'));
+ }
+ };
+ ArcList.prototype.pluckUniq = function(property) {
+ return _(this.items).chain().pluck(property).uniq().value();
+ };
+ ArcList.prototype.clone = function() {
+ return new ArcList(ArcList.__super__.clone.call(this).items);
+ };
+ return ArcList;
+ })();
+ _(exports).extend({
+ Node: Node,
+ Arc: Arc,
+ List: List,
+ NodeList: NodeList,
+ ArcList: ArcList,
+ Graph: Graph
+ });
+}).call(this);
View
186 lib/icing.js
@@ -0,0 +1,186 @@
+(function() {
+ var RecipeNode, Rule, RuleGraph, cakeTask, exec, fs, graph, runRecipeContext, stylize, _ref;
+ option('-v', '--verbose', 'Display progress as tasks are executed');
+ option('-w', '--watch', 'Monitor files for changes and automatically rebuild');
+ _ref = require('./rules'), RuleGraph = _ref.RuleGraph, Rule = _ref.Rule, RecipeNode = _ref.RecipeNode;
+ exec = require('child_process').exec;
+ fs = require('fs');
+ cakeTask = global.task;
+ graph = new RuleGraph;
+ global.task = function(target, description, prereqs, recipe) {
+ if (prereqs == null) {
+ prereqs = void 0;
+ }
+ if (recipe == null) {
+ recipe = void 0;
+ }
+ if (!(prereqs != null) && !(recipe != null)) {
+ recipe = description;
+ description = target;
+ prereqs = [];
+ }
+ if (!(recipe != null)) {
+ if (description.shift != null) {
+ recipe = prereqs;
+ prereqs = description;
+ description = target;
+ } else {
+ recipe = prereqs;
+ prereqs = [];
+ }
+ }
+ graph.rule(new Rule(target, prereqs, recipe));
+ return cakeTask(target, description, function(options) {
+ var aRecipeRan, allRecipesProcessed, fileSources, recipeNode, recipeNodes, runTask, taskIsRunning;
+ taskIsRunning = false;
+ recipeNodes = {
+ isEmpty: function() {
+ return true;
+ }
+ };
+ aRecipeRan = false;
+ allRecipesProcessed = false;
+ recipeNode = {};
+ runTask = function() {
+ var runNextRecipeCallback;
+ taskIsRunning = true;
+ recipeNodes = graph.recipeNodesTo(target);
+ aRecipeRan = false;
+ allRecipesProcessed = false;
+ recipeNode = {};
+ runNextRecipeCallback = function(ok, report) {
+ var context;
+ if (ok == null) {
+ ok = true;
+ }
+ if (report == null) {
+ report = {};
+ }
+ if (!ok) {
+ console.error(stylize("===== " + report.target + " Task Failed =====", 'red'));
+ console.error(stylize(report.message, 'red'));
+ if (options.watch != null) {
+ taskIsRunning = false;
+ return;
+ }
+ process.exit(1);
+ }
+ if (!recipeNodes.isEmpty()) {
+ if (recipeNode.name != null) {
+ recipeNode.refreshOutputs(graph);
+ }
+ recipeNode = recipeNodes.shift();
+ if (recipeNode.shouldRun(graph)) {
+ context = runRecipeContext(graph, recipeNode, runNextRecipeCallback, options);
+ recipeNode.run(context, options);
+ return aRecipeRan = true;
+ } else {
+ return runNextRecipeCallback();
+ }
+ } else {
+ allRecipesProcessed = true;
+ if (!aRecipeRan && (options.verbose != null)) {
+ console.error(stylize("cake: Nothing to be done for `" + target + "'.", 'yellow'));
+ }
+ if (options.watch != null) {
+ console.log(stylize("task '" + target + "' finished", 'grey'));
+ }
+ return taskIsRunning = false;
+ }
+ };
+ return runNextRecipeCallback();
+ };
+ if (options.watch != null) {
+ fileSources = graph.fileSources(target).names();
+ if (fileSources.length === 0) {
+ console.error(stylize("cake: Nothing to watch for `" + target + "'", 'yellow'));
+ }
+ fileSources.forEach(function(file) {
+ return fs.watchFile(file, {
+ interval: 250
+ }, function(curr, prev) {
+ if (taskIsRunning) {
+ return;
+ }
+ if (curr.mtime > prev.mtime) {
+ console.log(stylize("! `" + file + "` Changed", 'green'));
+ graph.refresh(file);
+ return runTask();
+ }
+ });
+ });
+ }
+ process.on('exit', function() {
+ var tasksLeft;
+ if (!allRecipesProcessed && !recipeNodes.isEmpty()) {
+ tasksLeft = recipeNodes.pluck('name').join(',');
+ return console.error(("\nError: task `" + recipeNode.name + "` did not complete.\n") + ("Tasks [" + tasksLeft + "] should have run, but did not.\n") + ("Task `" + recipeNode.name + "` should call this.finished() or this.failed(message).\n"));
+ }
+ });
+ return runTask();
+ });
+ };
+ runRecipeContext = function(graph, recipeNode, runNextRecipeCallback, options) {
+ var execFn, failedFn, finishedFn;
+ finishedFn = function() {
+ return runNextRecipeCallback();
+ };
+ failedFn = function(message) {
+ return runNextRecipeCallback(false, {
+ target: recipeNode.name,
+ message: message
+ });
+ };
+ execFn = function(commands) {
+ var runNextCommandCallback;
+ if (!(commands.shift != null)) {
+ commands = [commands];
+ }
+ runNextCommandCallback = function() {
+ var command;
+ if (commands.length > 0) {
+ command = commands.shift();
+ if (options.verbose != null) {
+ console.log(stylize("$ " + command, 'grey'));
+ }
+ return exec(command, function(error, stdout, stderr) {
+ if ((options.verbose != null) && stdout !== '') {
+ console.log(stdout);
+ }
+ if (!(error != null)) {
+ return runNextCommandCallback();
+ } else {
+ return failedFn(stderr);
+ }
+ });
+ } else {
+ return finishedFn();
+ }
+ };
+ return runNextCommandCallback();
+ };
+ return {
+ callback: runNextRecipeCallback,
+ finished: finishedFn,
+ failed: failedFn,
+ prereqs: recipeNode.prereqs(graph).names(),
+ modifiedPrereqs: recipeNode.modifiedPrereqs(graph).names(),
+ exec: execFn
+ };
+ };
+ stylize = function(str, style) {
+ var styles;
+ styles = {
+ 'bold': [1, 22],
+ 'italic': [3, 23],
+ 'underline': [4, 24],
+ 'cyan': [96, 39],
+ 'yellow': [33, 39],
+ 'green': [32, 39],
+ 'red': [31, 39],
+ 'grey': [90, 39],
+ 'green-hi': [92, 32]
+ };
+ return "\033[" + styles[style][0] + "m" + str + "\033[" + styles[style][1] + "m";
+ };
+}).call(this);
View
216 lib/rules.js
@@ -0,0 +1,216 @@
+(function() {
+ var Arc, FileNode, Graph, Node, NodeList, Recipe, RecipeNode, Rule, RuleGraph, assert, fs, globSync, _, _ref;
+ var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
+ for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
+ function ctor() { this.constructor = child; }
+ ctor.prototype = parent.prototype;
+ child.prototype = new ctor;
+ child.__super__ = parent.prototype;
+ return child;
+ };
+ _ref = require('./dag'), Graph = _ref.Graph, Node = _ref.Node, Arc = _ref.Arc, NodeList = _ref.NodeList;
+ _ = require('underscore');
+ assert = require('assert');
+ globSync = require('glob').globSync;
+ fs = require('fs');
+ RuleGraph = (function() {
+ function RuleGraph() {
+ RuleGraph.__super__.constructor.apply(this, arguments);
+ }
+ __extends(RuleGraph, Graph);
+ RuleGraph.prototype.refresh = function(file) {
+ return this.nodeMap[file].refresh();
+ };
+ RuleGraph.prototype.fileSources = function(target) {
+ return this.subgraph(target).sources().ofType(FileNode);
+ };
+ RuleGraph.prototype.recipeNodesTo = function(target) {
+ return this.subgraph(target).topologicalOrdering().ofType(RecipeNode);
+ };
+ RuleGraph.prototype.rule = function(rule) {
+ var graph, outputs, target;
+ assert.ok(rule instanceof Rule);
+ graph = this;
+ target = new RecipeNode(rule.target, rule.recipe);
+ graph.node(target);
+ rule.prereqs = _(rule.prereqs).chain().map(function(prereq) {
+ var globbed;
+ globbed = globSync(prereq);
+ if (globbed.length > 0) {
+ return globbed;
+ } else {
+ return prereq;
+ }
+ }).flatten().value();
+ rule.filePrereqs = [];
+ rule.prereqs.forEach(function(prereq) {
+ var input, inputsOutputs, kind, specialPrereq;
+ specialPrereq = prereq.match(/^(task|outputs)\((.*)\)$/);
+ if (specialPrereq) {
+ kind = specialPrereq[1];
+ prereq = specialPrereq[2];
+ }
+ input = graph.node(prereq);
+ if (input && input instanceof RecipeNode) {
+ if (specialPrereq === null) {
+ throw new Error("Task prerequisites must be referenced with task(" + prereq + ") or outputs(" + prereq + ")");
+ }
+ if (!(input 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 (!inputsOutputs.isEmpty()) {
+ inputsOutputs.forEach(function(inputsOutput) {
+ graph.arc(inputsOutput.name, target.name);
+ return 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 {
+ input = new FileNode(prereq);
+ rule.filePrereqs.push(prereq);
+ graph.node(input);
+ }
+ return graph.arc(input.name, target.name);
+ });
+ outputs = rule.recipe.outputs.call(rule);
+ outputs.forEach(function(output) {
+ output = new FileNode(output);
+ graph.node(output);
+ return graph.arc(target.name, output.name);
+ });
+ return this;
+ };
+ return RuleGraph;
+ })();
+ Rule = (function() {
+ function Rule(target, prereqs, recipe) {
+ this.target = target;
+ this.prereqs = prereqs;
+ this.recipe = recipe;
+ if (_(this.recipe).isFunction()) {
+ this.recipe = new Recipe(this.recipe);
+ }
+ this.prereqs = this.prereqs.reverse();
+ }
+ return Rule;
+ })();
+ Recipe = (function() {
+ function Recipe(exec, outputs) {
+ this.exec = exec != null ? exec : (function() {});
+ this.outputs = outputs != null ? outputs : (function() {
+ return [];
+ });
+ }
+ return Recipe;
+ })();
+ RecipeNode = (function() {
+ __extends(RecipeNode, Node);
+ function RecipeNode(name, recipe) {
+ this.name = name;
+ this.recipe = recipe;
+ }
+ RecipeNode.prototype.equals = function(node) {
+ return RecipeNode.__super__.equals.call(this, node) && node instanceof RecipeNode;
+ };
+ RecipeNode.prototype.clone = function(node) {
+ return new RecipeNode(this.name, this.recipe);
+ };
+ RecipeNode.prototype.prereqs = function(graph) {
+ return graph.arcs.to(graph.node(this.name)).from().ofType(FileNode);
+ };
+ RecipeNode.prototype.outputs = function(graph) {
+ return graph.arcs.from(graph.node(this.name)).to().ofType(FileNode);
+ };
+ RecipeNode.prototype.refreshOutputs = function(graph) {
+ return this.outputs(graph).forEach(function(fileNode) {
+ return fileNode.refresh();
+ });
+ };
+ RecipeNode.prototype.modifiedPrereqs = function(graph) {
+ var outputMin, outputs, prereqMax, prereqs;
+ prereqs = this.prereqs(graph);
+ outputs = this.outputs(graph);
+ if (prereqs.isEmpty() || outputs.isEmpty()) {
+ return prereqs;
+ }
+ if (prereqs.count() === outputs.count()) {
+ return new NodeList(_(_.zip(prereqs.items, outputs.items)).chain().map(function(pair) {
+ var input, inputTime, output, outputTime;
+ input = pair[0], output = pair[1];
+ inputTime = input.mtime || Date.now();
+ outputTime = output.mtime || 0;
+ if (inputTime > outputTime) {
+ return input;
+ } else {
+ return false;
+ }
+ }).compact().value());
+ } else {
+ prereqMax = _(prereqs.items).max(function(fileNode) {
+ return fileNode.mtime || (fileNode.mtime = Date.now());
+ });
+ outputMin = _(outputs.items).min(function(fileNode) {
+ return fileNode.mtime || (fileNode.mtime = 0);
+ });
+ if (prereqMax.mtime > outputMin.mtime) {
+ return prereqs;
+ } else {
+ return new NodeList;
+ }
+ }
+ };
+ RecipeNode.prototype.shouldRun = function(graph) {
+ var outputs, prereqs;
+ prereqs = this.prereqs(graph);
+ if (prereqs.isEmpty()) {
+ return true;
+ }
+ outputs = this.outputs(graph);
+ if (outputs.isEmpty()) {
+ return true;
+ }
+ return !this.modifiedPrereqs(graph).isEmpty();
+ };
+ RecipeNode.prototype.run = function(context, options) {
+ return this.recipe.exec.call(context, options);
+ };
+ return RecipeNode;
+ })();
+ FileNode = (function() {
+ __extends(FileNode, Node);
+ function FileNode(name) {
+ this.name = name;
+ this.refresh();
+ }
+ FileNode.prototype.equals = function(node) {
+ return FileNode.__super__.equals.call(this, node) && node instanceof FileNode;
+ };
+ FileNode.prototype.clone = function(node) {
+ return new FileNode(this.name);
+ };
+ FileNode.prototype.refresh = function() {
+ try {
+ this.stats = fs.statSync(this.name);
+ return this.mtime = this.stats.mtime;
+ } catch (Error) {
+ this.stats = {};
+ return this.mtime = void 0;
+ }
+ };
+ return FileNode;
+ })();
+ _(exports).extend({
+ RuleGraph: RuleGraph,
+ Rule: Rule,
+ RecipeNode: RecipeNode,
+ FileNode: FileNode,
+ Recipe: Recipe
+ });
+}).call(this);
View
15 package.json
@@ -0,0 +1,15 @@
+{
+ "name": "icing",
+ "description": "Dependency management for cake command.",
+ "keywords": ["cake","dependency","build","watch"],
+ "version": "0.1.0",
+ "repository": "git://github.com/KrisJordan/icing.git",
+ "author": "Kris Jordan",
+ "directories": {
+ "lib": "./lib",
+ "doc": "./doc"
+ },
+ "engines": {
+ "node": ">=0.2.5"
+ }
+}
View
43 spec/rules.vows.coffee
@@ -45,7 +45,7 @@ vows
topic: ->
graph = new RuleGraph
graph.rule new Rule 'B', ['A'], (->)
- graph.rule new Rule 'C', ['B'], (->)
+ graph.rule new Rule 'C', ['task(B)'], (->)
'has RecipeNodes':
topic: (graph) -> graph.nodes.ofType RecipeNode
'of length 2': (recipes) ->
@@ -84,26 +84,25 @@ vows
topic: ->
graph = new RuleGraph
graph.rule new Rule 'B', ['A'], new Recipe (->), (->['C'])
- graph.rule new Rule 'D', ['B'], (->)
- 'has RecipeNodes':
- topic: (graph) -> graph.nodes.ofType RecipeNode
- 'of length 2': (recipes) ->
- assert.length recipes.items, 2
- 'has FileNodes':
- topic: (graph) -> graph.nodes.ofType FileNode
- 'of length 2': (files) ->
- assert.length files.items, 2
- 'has Arcs':
- topic: (graph) -> graph.arcs
- 'of length 3': (arcs) ->
- assert.length arcs.items, 3
- 'from A,B,C': (arcs) ->
- from = _(arcs.pluck 'from').pluck 'name'
- ['A','B','C'].forEach (item) ->
- assert.includes from, item
- 'to B,C,D': (arcs) ->
- to = _(arcs.pluck 'to').pluck 'name'
- ['B','C','D'].forEach (item) ->
- assert.includes to, item
+ 'should throw referencing a new rule': (graph) ->
+ assert.throws -> graph.rule new Rule 'D', ['B'], (->), Error
+# topic: (graph) -> graph.nodes.ofType RecipeNode
+# 'has FileNodes':
+# topic: (graph) -> graph.nodes.ofType FileNode
+# 'of length 2': (files) ->
+# assert.length files.items, 2
+# 'has Arcs':
+# topic: (graph) -> graph.arcs
+# 'of length 3': (arcs) ->
+# assert.length arcs.items, 3
+# 'from A,B,B': (arcs) ->
+# from = _(arcs.pluck 'from').pluck 'name'
+# ['A','B','B'].forEach (item) ->
+# assert.includes from, item
+# 'to B,C,D': (arcs) ->
+# to = _(arcs.pluck 'to').pluck 'name'
+# ['B','C','D'].forEach (item) ->
+# assert.includes to, item
+
)
.export(module)
View
5 src/icing.coffee
@@ -92,7 +92,6 @@ global.task = (target, description, prereqs=undefined, recipe=undefined) ->
allRecipesProcessed = false
recipeNode = {}
-
runTask = ->
taskIsRunning = true
recipeNodes = graph.recipeNodesTo target
@@ -125,6 +124,8 @@ global.task = (target, description, prereqs=undefined, recipe=undefined) ->
if not aRecipeRan and options.verbose?
# Homage
console.error stylize "cake: Nothing to be done for `#{target}'.", 'yellow'
+ if options.watch?
+ console.log stylize "task '#{target}' finished",'grey'
taskIsRunning = false
do runNextRecipeCallback
@@ -174,7 +175,7 @@ runRecipeContext = (graph, recipeNode, runNextRecipeCallback, options) ->
if not error?
runNextCommandCallback()
else
- failedFn()
+ failedFn(stderr)
else
finishedFn()
View
29 src/rules.coffee
@@ -60,25 +60,42 @@ class RuleGraph extends Graph
.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.
- if graph.node prereq
- input = graph.node prereq
- # There's a special case when a RecipeNode's recipe has outputs. When
- # other RecipeNode targets have one as a prereq its dependency is on
- # those FileNode outputs and not the RecipeNode itself.
- if input instanceof RecipeNode
+ 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
Please sign in to comment.
Something went wrong with that request. Please try again.