Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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
Kris Jordan authored
19 Cakefile
View
@@ -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/*"
]
3  README.markdown
View
@@ -0,0 +1,3 @@
+# icing
+
+icing adds dependency management to Cakefiles
2  docs/dag.html
View
@@ -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>
17 docs/icing.html
View
@@ -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>
26 docs/rules.html
View
@@ -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>
252 lib/dag.js
View
@@ -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);
186 lib/icing.js
View
@@ -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);
216 lib/rules.js
View
@@ -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);
15 package.json
View
@@ -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"
+ }
+}
43 spec/rules.vows.coffee
View
@@ -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)
5 src/icing.coffee
View
@@ -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()
29 src/rules.coffee
View
@@ -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.