[gnome-ostree] Rewrite tasks system



commit c02802a6a7c6d0d0fb2a6c1ddbe886c926120b98
Author: Colin Walters <walters verbum org>
Date:   Thu Jan 24 09:13:51 2013 -0500

    Rewrite tasks system
    
    Merge SubTask and DynTask for now - drop the dependencies, clean
    things up a bit.  Move resolve, build, builddisks to be tasks.
    The autobuilder now is updated to use this.
    
    Add a new bdiff task.
    
    There's a new "ostbuild make" builtin that runs tasks.  The benefit of
    this when you do a build directly, like:
    
    $ ostbuild make build/gnomeos-3.8
    
    It's consistently run in a tempdir, logged etc., in the same way that
    the autobuilder does it.

 Makefile-ostbuild.am                               |   16 +-
 qa/repoweb/index.html                              |  121 ++++---
 qa/repoweb/repoweb.js                              |  101 ++----
 src/libgsystem                                     |    2 +-
 src/ostbuild/js/buildutil.js                       |   81 ++---
 src/ostbuild/js/builtins/autobuilder.js            |  316 +++++------------
 src/ostbuild/js/builtins/build_disks.js            |  112 ------
 src/ostbuild/js/builtins/git_mirror.js             |   28 +-
 src/ostbuild/js/builtins/make.js                   |   86 +++++
 src/ostbuild/js/builtins/resolve.js                |  140 -------
 src/ostbuild/js/builtins/run_task.js               |   55 +++
 src/ostbuild/js/dyntask.js                         |  244 -------------
 src/ostbuild/js/jsondb.js                          |   12 +
 src/ostbuild/js/jsonutil.js                        |   38 ++
 src/ostbuild/js/jsutil.js                          |   23 ++
 src/ostbuild/js/main.js                            |    2 +
 src/ostbuild/js/procutil.js                        |    2 +-
 src/ostbuild/js/snapshot.js                        |   75 ++++-
 src/ostbuild/js/subtask.js                         |  181 ----------
 src/ostbuild/js/task.js                            |  381 ++++++++++++++++++++
 src/ostbuild/js/tasks/task-bdiff.js                |  155 ++++++++
 .../js/{builtins/build.js => tasks/task-build.js}  |  109 +++----
 src/ostbuild/js/tasks/task-builddisks.js           |  141 ++++++++
 src/ostbuild/js/tasks/task-checksum.js             |  123 -------
 src/ostbuild/js/tasks/task-resolve.js              |   84 +++++
 .../qa_smoketest.js => tasks/task-smoketest.js}    |  126 ++++---
 src/ostbuild/js/vcs.js                             |   39 ++-
 src/ostbuild/ostbuild.in                           |    2 +-
 28 files changed, 1452 insertions(+), 1343 deletions(-)
---
diff --git a/Makefile-ostbuild.am b/Makefile-ostbuild.am
index ef0ef1f..24c73ce 100644
--- a/Makefile-ostbuild.am
+++ b/Makefile-ostbuild.am
@@ -43,9 +43,10 @@ jsostbuild_DATA= \
 	src/ostbuild/js/buildutil.js \
 	src/ostbuild/js/builtin.js \
 	src/ostbuild/js/config.js \
-	src/ostbuild/js/dyntask.js \
+	src/ostbuild/js/task.js \
 	src/ostbuild/js/jsondb.js \
 	src/ostbuild/js/jsonutil.js \
+	src/ostbuild/js/jsutil.js \
 	src/ostbuild/js/main.js \
 	src/ostbuild/js/libqa.js \
 	src/ostbuild/js/guestfish.js \
@@ -53,28 +54,29 @@ jsostbuild_DATA= \
 	src/ostbuild/js/procutil.js \
 	src/ostbuild/js/snapshot.js \
 	src/ostbuild/js/streamutil.js \
-	src/ostbuild/js/subtask.js \
 	src/ostbuild/js/vcs.js \
 	$(NULL)
 
 jsostbuiltinsdir=$(jsostbuilddir)/builtins
 jsostbuiltins_DATA= \
 	src/ostbuild/js/builtins/autobuilder.js \
-	src/ostbuild/js/builtins/build.js \
-	src/ostbuild/js/builtins/build_disks.js \
 	src/ostbuild/js/builtins/checkout.js \
 	src/ostbuild/js/builtins/git_mirror.js \
+	src/ostbuild/js/builtins/make.js \
 	src/ostbuild/js/builtins/qa_make_disk.js \
 	src/ostbuild/js/builtins/qa_pull_deploy.js \
-	src/ostbuild/js/builtins/qa_smoketest.js \
 	src/ostbuild/js/builtins/prefix.js \
-	src/ostbuild/js/builtins/resolve.js \
+	src/ostbuild/js/builtins/run_task.js \
 	src/ostbuild/js/builtins/shell.js \
 	$(NULL)
 
 jsosttasksdir=$(jsostbuilddir)/tasks
 jsosttasks_DATA= \
-	src/ostbuild/js/tasks/task-checksum.js \
+	src/ostbuild/js/tasks/task-build.js \
+	src/ostbuild/js/tasks/task-resolve.js \
+	src/ostbuild/js/tasks/task-bdiff.js \
+	src/ostbuild/js/tasks/task-builddisks.js \
+	src/ostbuild/js/tasks/task-smoketest.js \
 	$(NULL)
 
 endif
diff --git a/qa/repoweb/index.html b/qa/repoweb/index.html
index 30765e5..5e9722e 100644
--- a/qa/repoweb/index.html
+++ b/qa/repoweb/index.html
@@ -1,63 +1,66 @@
 <!DOCTYPE html>
 <html>
-    <head>
-        <meta charset="utf-8" />
-        <meta name="viewport" content="width=device-width, initial-scale=1" />
-        <title>
-        </title>
-        <link rel="stylesheet" href="jquery.mobile-1.2.0.css" />
-        <link rel="stylesheet" href="repoweb.css" />
-        <style>
-            /* App custom styles */
-        </style>
-        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js";>
-        </script>
-        <script src="https://ajax.aspnetcdn.com/ajax/jquery.mobile/1.2.0/jquery.mobile-1.2.0.min.js";>
-        </script>
-        <script src="repoweb.js">
-        </script>
-    </head>
-    <body onload="$(document).ready(function(){repoweb_index_init();})">
-        <!-- Home -->
-        <div data-role="page" data-theme="a" id="page1">
-            <div data-theme="" data-role="header">
-                <h3>
-                    GNOME-OSTree build server
-                </h3>
-                <div data-role="navbar" data-iconpos="top">
-                    <ul>
-                        <li>
-                            <a href="#page1" data-transition="fade" data-theme="" data-icon="">
-                                Home
-                            </a>
-                        </li>
-                        <li>
-                            <a href="work/tasks" rel="external">
-                                Build logs
-                            </a>
-                        </li>
-                        <li>
-                            <a href="work/src" rel="external">
-                                Source code
-                            </a>
-                        </li>
-                    </ul>
-                </div>
-            </div>
-            <div data-role="content">
-                <div class="buildstatus" id="buildstatus">
-                </div>
-<p>This is the <a href="https://live.gnome.org/OSTree/GnomeOSTree"; class="ui-link">GNOME-OSTree</a> build server.  It builds from the <a href="http://git.gnome.org/browse/gnome-ostree"; class="ui-link">gnome-ostree</a> git module.</p>
-                <ul data-role="listview" data-divider-theme="a" data-inset="true" id="build-summary">
-                </ul>
-            </div>
-	<div class="footer">
-	<p>Optimized for standards. Powered by <a href="http://jquerymobile.com"; data-transition="fade">jquery-mobile</a>.</p>
-	</div>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>
+    </title>
+    <link rel="stylesheet" href="jquery.mobile-1.2.0.css" />
+    <link rel="stylesheet" href="repoweb.css" />
+    <style>
+      /* App custom styles */
+    </style>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js";>
+    </script>
+    <script src="https://ajax.aspnetcdn.com/ajax/jquery.mobile/1.2.0/jquery.mobile-1.2.0.min.js";>
+    </script>
+    <script src="repoweb.js">
+    </script>
+  </head>
+  <body onload="$(document).ready(function(){repowebIndexInit();})">
+    <!-- Home -->
+    <div data-role="page" data-theme="a" id="page1">
+      <div data-theme="" data-role="header">
+        <h3>
+          GNOME-OSTree build server
+        </h3>
+        <div data-role="navbar" data-iconpos="top">
+          <ul>
+            <li>
+              <a href="#page1" data-transition="fade" data-theme="" data-icon="">
+                Home
+              </a>
+            </li>
+            <li>
+              <a href="work/tasks" rel="external">
+                Build logs
+              </a>
+            </li>
+            <li>
+              <a href="work/src" rel="external">
+                Source code
+              </a>
+            </li>
+          </ul>
+        </div>
+      </div>
+      <div data-role="content">
+	<div class="content-primary">
+          <div class="buildstatus" id="build-icon">
+          </div>
+	  <p>This is the <a href="https://live.gnome.org/OSTree/GnomeOSTree"; class="ui-link">GNOME-OSTree</a> build server.  It builds from the <a href="http://git.gnome.org/browse/gnome-ostree"; class="ui-link">gnome-ostree</a> git module.</p>
+          <p>Current build: 
+	  <span id="build-meta"></span>
+          </p>
         </div>
-
-        <script>
-            //App custom javascript
-        </script>
-    </body>
+      </div>
+      <div class="footer">
+	<p>Optimized for standards. Powered by <a href="http://jquerymobile.com"; data-transition="fade">jquery-mobile</a>.</p>
+      </div>
+    </div>
+    
+    <script>
+      //App custom javascript
+    </script>
+  </body>
 </html>
diff --git a/qa/repoweb/repoweb.js b/qa/repoweb/repoweb.js
index a3bacf9..1b96276 100644
--- a/qa/repoweb/repoweb.js
+++ b/qa/repoweb/repoweb.js
@@ -1,5 +1,7 @@
 // -*- indent-tabs-mode: nil -*-
 
+const DEFAULT_PREFIX = 'gnomeos-3.8';
+
 function htmlescape(str) {
     var pre = document.createElement('pre');
     var text = document.createTextNode(str);
@@ -26,22 +28,19 @@ function get_page_arg(key) {
 }
 
 var repoDataSignal = {};
-var repoData = null;
+var currentBuildMeta = null;
 var prefix = null;
 
-function repoweb_on_data_loaded(data) {
-    console.log("data loaded");
-    repoData = data;
-    prefix = repoData['prefix'];
-    $(repoDataSignal).trigger("loaded");
-}
-
-function repoweb_init() {
-    var id = get_page_arg("prefix");
-    if (id == null)
-        id = "default";
-    var url = "work/autobuilder-" + id + ".json";
-    $.getJSON(url, repoweb_on_data_loaded);
+function repowebInit() {
+    prefix = get_page_arg("prefix");
+    if (prefix == null)
+        prefix = DEFAULT_PREFIX;
+    var url;
+    url = "work/tasks/build/" + prefix + "/current/meta.json";
+    $.getJSON(url, function(data) {
+        currentBuildMeta = data;
+        $(repoDataSignal).trigger("current-buildmeta-loaded");
+    });
 }
 
 function timeago(d, now) {
@@ -61,22 +60,6 @@ function timeago(d, now) {
     }
 }
 
-function buildDiffComponentAppend(container, description, a) {
-    var additional = 0;
-    if (a.length > 10) {
-        a = a.slice(0, 10); 
-        additional = a.length - 10;
-    }
-    var p = document.createElement('p');
-    container.appendChild(p);
-    p.appendChild(document.createTextNode(description + ": " + a.join(", ")));
-    if (additional > 0) {
-        var b = document.createElement('b');
-        p.appendChild(b);
-        b.appendChild.document.createTextNode(" and " + additional + " more");
-    }
-}
-
 function buildDiffAppend(container, buildDiff) {
     if (!buildDiff)
         return document.createTextNode("No changes or new build");
@@ -139,41 +122,31 @@ function renderBuild(container, build) {
 
 }
 
-function updateResolve() {
-    $("#resolve-summary").empty();
-    var summary = $("#resolve-summary").get(0);
-
-    var div = document.createElement('div');
-    summary.appendChild(div);
-    div.appendChild(document.createTextNode("Current version: "));
-    var a = document.createElement('a');
-    div.appendChild(a);
-    a.setAttribute('href', 'work/snapshots/' + repoData['version-path']);
-    a.setAttribute('rel', 'external');
-    a.appendChild(document.createTextNode(repoData['version']));
-}
-
-function repoweb_index_init() {
-    repoweb_init();
-    $(repoDataSignal).on("loaded", function () {
-
-	var buildSummary = $("#build-summary").get(0);
-        var buildData = repoData.build;
-        for (var i = buildData.length - 1; i >= 0; i--) {
-            var build = buildData[i];
-            renderBuild(buildSummary, build);
-        }
-        if (buildData.length > 0) {
-            var build = buildData[0];
-            $("#buildstatus").removeClass("buildstatus-happy");
-            $("#buildstatus").removeClass("buildstatus-sad");
-            if (build['state'] == 'failed') 
-               $("#buildstatus").addClass("buildstatus-sad");
-            else
-               $("#buildstatus").addClass("buildstatus-happy");
+function repowebIndexInit() {
+    repowebInit();
+    $(repoDataSignal).on("current-buildmeta-loaded", function () {
+	var buildMetaNode = $("#build-meta").get(0);
+
+        $(buildMetaNode).empty();
+        var ref = 'work/tasks/build/' + prefix;
+        if (currentBuildMeta.success)
+            ref += '/successful';
+        else
+            ref += '/failed';
+        ref += '/' + currentBuildMeta.taskVersion;
+        var a = document.createElement('a');
+        a.setAttribute('href', ref);
+        a.setAttribute('rel', 'external');
+        a.appendChild(document.createTextNode(currentBuildMeta.taskVersion));
+        buildMetaNode.appendChild(a);
+        buildMetaNode.appendChild(document.createTextNode(': ' + (currentBuildMeta.success ? "success" : "failed ")));
+        
+        $("#build-icon").removeClass("buildstatus-happy");
+        $("#build-icon").removeClass("buildstatus-sad");
+        if (currentBuildMeta.success) {
+            $("#build-icon").addClass("buildstatus-happy");
         } else {
-               $("#buildstatus").addClass("buildstatus-happy");
+            $("#build-icon").addClass("buildstatus-sad");
         }
-	$(buildSummary).listview('refresh');
     });
 }
diff --git a/src/libgsystem b/src/libgsystem
index dd1b303..0258d06 160000
--- a/src/libgsystem
+++ b/src/libgsystem
@@ -1 +1 @@
-Subproject commit dd1b3032b4cd175c2a9b2c611a62525454bad772
+Subproject commit 0258d06e5342bc4617f88594324eed43269e133e
diff --git a/src/ostbuild/js/buildutil.js b/src/ostbuild/js/buildutil.js
index 0f3d1c7..5bcfcb4 100644
--- a/src/ostbuild/js/buildutil.js
+++ b/src/ostbuild/js/buildutil.js
@@ -19,6 +19,8 @@ const GLib = imports.gi.GLib;
 const Gio = imports.gi.Gio;
 const Lang = imports.lang;
 
+const GSystem = imports.gi.GSystem;
+
 const BUILD_ENV = {
     'HOME' : '/', 
     'HOSTNAME' : 'ostbuild',
@@ -42,50 +44,7 @@ function parseSrcKey(srckey) {
     return [keytype, uri];
 }
 
-function resolveComponent(manifest, componentMeta) {
-    let result = {};
-    Lang.copyProperties(componentMeta, result);
-    let origSrc = componentMeta['src'];
-
-    let didExpand = false;
-    let vcsConfig = manifest['vcsconfig'];
-    for (let vcsprefix in vcsConfig) {
-	let expansion = vcsConfig[vcsprefix];
-        let prefix = vcsprefix + ':';
-        if (origSrc.indexOf(prefix) == 0) {
-            result['src'] = expansion + origSrc.substr(prefix.length);
-            didExpand = true;
-            break;
-	}
-    }
-
-    let name = componentMeta['name'];
-    let src, idx, name;
-    if (name == undefined) {
-        if (didExpand) {
-            src = origSrc;
-            idx = src.lastIndexOf(':');
-            name = src.substr(idx+1);
-        } else {
-            src = result['src'];
-            idx = src.lastIndexOf('/');
-            name = src.substr(idx+1);
-	}
-	let i = name.lastIndexOf('.git');
-        if (i != -1 && i == name.length - 4) {
-            name = name.substr(0, name.length - 4);
-	}
-        name = name.replace(/\//g, '-');
-        result['name'] = name;
-    }
-
-    let branchOrTag = result['branch'] || result['tag'];
-    if (!branchOrTag) {
-        result['branch'] = 'master';
-    }
 
-    return result;
-}
 
 function getPatchPathsForComponent(patchdir, component) {
     let patches = component['patches'];
@@ -127,3 +86,39 @@ function getBaseUserChrootArgs() {
     let path = findUserChrootPath();
     return [path.get_path(), '--unshare-pid', '--unshare-ipc', '--unshare-net'];
 }
+
+function compareVersions(a, b) {
+    let adot = a.indexOf('.');
+    while (adot != -1) {
+	let bdot = b.indexOf('.');
+	if (bdot == -1)
+	    return 1;
+	let aSub = parseInt(a.substr(0, adot));
+	let bSub = parseInt(b.substr(0, bdot));
+	if (aSub > bSub)
+	    return 1;
+	else if (aSub < bSub)
+	    return -1;
+	a = a.substr(adot + 1);
+	b = b.substr(bdot + 1);
+	adot = a.indexOf('.');
+    }
+    if (b.indexOf('.') != -1)
+	return -1;
+    let aSub = parseInt(a);
+    let bSub = parseInt(b);
+    if (aSub > bSub)
+	return 1;
+    else if (aSub < bSub)
+	return -1;
+    return 0;
+}
+
+function atomicSymlinkSwap(linkPath, newTarget, cancellable) {
+    let parent = linkPath.get_parent();
+    let tmpLinkPath = parent.get_child('current-new.tmp');
+    GSystem.shutil_rm_rf(tmpLinkPath, cancellable);
+    let relpath = parent.get_relative_path(newTarget);
+    tmpLinkPath.make_symbolic_link(relpath, cancellable);
+    GSystem.file_rename(tmpLinkPath, linkPath, cancellable);
+}
diff --git a/src/ostbuild/js/builtins/autobuilder.js b/src/ostbuild/js/builtins/autobuilder.js
index 17268c8..b437175 100644
--- a/src/ostbuild/js/builtins/autobuilder.js
+++ b/src/ostbuild/js/builtins/autobuilder.js
@@ -23,7 +23,7 @@ const Format = imports.format;
 const GSystem = imports.gi.GSystem;
 
 const Builtin = imports.builtin;
-const SubTask = imports.subtask;
+const Task = imports.task;
 const JsonDB = imports.jsondb;
 const ProcUtil = imports.procutil;
 const JsonUtil = imports.jsonutil;
@@ -54,14 +54,12 @@ const Autobuilder = new Lang.Class({
 
 	this._stages = ['resolve', 'build', 'builddisks', 'smoke'];
 
-	this._build_needed = true;
-	this._do_builddisks = false;
-	this._do_qa = false;
-	this._full_resolve_needed = true;
-	this._queued_force_resolve = [];
-	this._resolve_timeout = 0;
-	this._source_snapshot_path = null;
-	this._prev_source_snapshot_path = null;
+	this._buildNeeded = true;
+	this._fullResolveNeeded = true;
+	this._resolveNeeded = false;
+	this._resolveTimeout = 0;
+	this._sourceSnapshotPath = null;
+	this._prevSourceSnapshotPath = null;
     },
 
     execute: function(args, loop, cancellable) {
@@ -69,14 +67,17 @@ const Autobuilder = new Lang.Class({
 
 	this._autoupdate_self = args.autoupdate_self;
 	if (!args.stage)
-	    args.stage = 'smoke';
+	    args.stage = 'build';
 	this._stageIndex = this._stages.indexOf(args.stage);
 	if (this._stageIndex < 0)
 	    throw new Error("Unknown stage " + args.stage);
 	this._do_builddisks = this._stageIndex >= this._stages.indexOf('builddisks');
 	this._do_smoke = this._stageIndex >= this._stages.indexOf('smoke');
 
-	this._status_path = this.workdir.get_child('autobuilder-' + this.prefix + '.json');
+	this._resolveTaskName = 'resolve/' + this.prefix;
+	this._buildTaskName = 'build/' + this.prefix;
+	this._bdiffTaskName = 'bdiff/' + this.prefix;
+
 	this._manifestPath = Gio.File.new_for_path('manifest.json');
 
 	this._ownId = Gio.DBus.session.own_name('org.gnome.OSTreeBuild', Gio.BusNameOwnerFlags.NONE,
@@ -89,35 +90,42 @@ const Autobuilder = new Lang.Class({
 	this._snapshot_dir = this.workdir.get_child('snapshots').get_child(this.prefix);
 	this._src_db = new JsonDB.JsonDB(this._snapshot_dir);
 
-	let taskdir = this.workdir.get_child('tasks');
-	this._resolve_taskset = new SubTask.TaskSet(taskdir.get_child(this.prefix + '-resolve'));
-	this._build_taskset = new SubTask.TaskSet(taskdir.get_child(this.prefix + '-build'));
-	this._builddisks_taskset = new SubTask.TaskSet(taskdir.get_child(this.prefix + '-build-disks'));
-	this._smoke_taskset = new SubTask.TaskSet(taskdir.get_child(this.prefix + '-smoke'));
-
-	this._source_snapshot_path = this._src_db.getLatestPath();
+	this._taskmaster = new Task.TaskMaster(this.workdir.get_child('tasks'),
+						  { onEmpty: Lang.bind(this, this._onTasksComplete) });
+	this._taskmaster.connect('task-complete', Lang.bind(this, this._onTaskCompleted));
 
-	this._status_path = this.workdir.get_child('autobuilder-' + this.prefix + '.json');
+	this._sourceSnapshotPath = this._src_db.getLatestPath();
 
-	this._resolve_timeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
-							 60 * 10, Lang.bind(this, this._fetchAll));
-	this._fetchAll();
-	if (this._source_snapshot_path != null)
-	    this._run_build();
+	this._resolveTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
+							 60 * 10, Lang.bind(this, this._triggerFullResolve));
+	this._runResolve();
+	if (this._sourceSnapshotPath != null)
+	    this._runBuild();
 
 	this._updateStatus();
 
 	loop.run();
     },
 
+    _onTasksComplete: function() {
+    },
+
+    _onTaskCompleted: function(taskmaster, task, success, error) {
+	if (task.name == this._resolveTaskName) {
+	    this._onResolveExited(task, success, error);
+	} else if (task.name == this._buildTaskName) {
+	    this._onBuildExited(task, success, error);
+	}
+	this._updateStatus();
+    },
+
     _updateStatus: function() {
 	let newStatus = "";
-	if (this._resolve_taskset.isRunning())
-	    newStatus += "[resolving] ";
-	if (this._build_taskset.isRunning())
-	    newStatus += " [building] ";
-	if (this._builddisks_taskset.isRunning())
-	    newStatus += " [disks] ";
+	let taskstateList = this._taskmaster.getTaskState();
+	for (let i = 0; i < taskstateList.length; i++) {
+	    let taskstate = taskstateList[i];
+	    newStatus += (taskstate.task.name + " ");
+	}
 	if (newStatus == "")
 	    newStatus = "[idle]";
 	if (newStatus != this._status) {
@@ -125,8 +133,6 @@ const Autobuilder = new Lang.Class({
 	    print(this._status);
 	    this._impl.emit_property_changed('Status', new GLib.Variant("s", this._status));
 	}
-
-	this._writeStatusFile();
     },
 
     get Status() {
@@ -135,234 +141,92 @@ const Autobuilder = new Lang.Class({
 
     queueResolve: function(srcUrls) {
 	let matchingComponents = [];
-	let snapshotData = this._src_db.loadFromPath(this._source_snapshot_path, null);
-	let snapshot = new Snapshot.Snapshot(snapshotData, this._source_snapshot_path);
+	let snapshotData = this._src_db.loadFromPath(this._sourceSnapshotPath, null);
+	let snapshot = new Snapshot.Snapshot(snapshotData, this._sourceSnapshotPath);
+	let matched = false;
 	for (let i = 0; i < srcUrls.length; i++) {
 	    let matches = snapshot.getMatchingSrc(srcUrls[i]);
-	    for (let j = 0; j < matches.length; j++)
-		matchingComponents.push(matches[j]['name']);
-	}
-	if (matchingComponents.length > 0) {
-	    this._queued_force_resolve.push.apply(this._queued_force_resolve, matchingComponents);
-	    print("queued resolves: " + matchingComponents.join(' '));
-	    if (!this._resolve_taskset.isRunning())
-		this._fetch();
-	} else {
-	    print("Ignored fetch requests for unknown URLs: " + srcUrls.join(','));
+	    for (let j = 0; j < matches.length; j++) {
+		this._queuedForceResolve.push.apply(this._queuedForceResolve, matches[i]['name']);
+		matched = true;
+	    }
 	}
+	if (matched)
+	    this._resolveNeeded = true;
+	this._runResolve();
     },
     
-    _fetchAll: function() {
-	this._full_resolve_needed = true;
-	if (!this._resolve_taskset.isRunning())
-	    this._fetch();
+    _triggerFullResolve: function() {
+	this._fullResolveNeeded = true;
+	this._runResolve();
 	return true;
     },
 
-    _fetch: function() {
+    _runResolve: function() {
 	let cancellable = null;
+	
+	if (!(this._resolveNeeded || this._fullResolveNeeded))
+	    return;
+
+	if (this._taskmaster.isTaskQueued(this._resolveTaskName))
+	    return;
 
 	if (this._autoupdate_self)
 	    ProcUtil.runSync(['git', 'pull', '-r'], cancellable)
 
-	let args = ['ostbuild', 'resolve', '--manifest=manifest.json',
-		    '--fetch', '--fetch-keep-going'];
-	let isFull;
-	if (this._full_resolve_needed) {
-	    this._full_resolve_needed = false;
-	    isFull = true;
-	} else if (this._queued_force_resolve.length > 0) {
-	    args.push.apply(args, this._queued_force_resolve);
-	    isFull = false;
+	if (this._fullResolveNeeded) {
+	    this._fullResolveNeeded = false;
+	    this._taskmaster.pushTask(this._resolveTaskName,
+				      { fetchAll: true });
 	} else {
-	    throw new Error("_fetch() when not needed");
+	    this._taskmaster.pushTask(this._resolveTaskName,
+				      { fetchComponents: this._queuedForceResolve });
 	}
-	this._queued_force_resolve = [];
-	let context = new GSystem.SubprocessContext({ argv: args });
-	let workdir = this._resolve_taskset.prepare();
-	let tmpManifest = workdir.get_child(this._manifestPath.get_basename());
-	GSystem.file_linkcopy(this._manifestPath, tmpManifest, Gio.FileCopyFlags.OVERWRITE, cancellable);	
-	let t = this._resolve_taskset.start(context,
-					    cancellable,
-					    Lang.bind(this, this._onResolveExited));
-	print(Format.vprintf("Resolve task %s started (%s)", [t.versionstr, isFull ? "full" : "incremental"]));
+	this._queuedForceResolve = [];
 
 	this._updateStatus();
-
-	return false;
     },
 
     _onResolveExited: function(resolveTask, success, msg) {
 	print(Format.vprintf("resolve exited; success=%s msg=%s", [success, msg]))
-	this._prev_source_snapshot_path = this._source_snapshot_path;
-	this._source_snapshot_path = this._src_db.getLatestPath();
-	let changed = (this._prev_source_snapshot_path == null ||
-		       !this._prev_source_snapshot_path.equal(this._source_snapshot_path));
+	this._prevSourceSnapshotPath = this._sourceSnapshotPath;
+	this._sourceSnapshotPath = this._src_db.getLatestPath();
+	let changed = (this._prevSourceSnapshotPath == null ||
+		       !this._prevSourceSnapshotPath.equal(this._sourceSnapshotPath));
         if (changed)
-            print(Format.vprintf("New version is %s", [this._source_snapshot_path.get_path()]))
-	if (!this._build_needed)
-	    this._build_needed = changed;
-	if (this._build_needed && !this._build_taskset.isRunning())
-	    this._run_build();
-
-	if (this._full_resolve_needed || this._queued_force_resolve.length > 0) {
-	    this._fetch();
-	}
-
+            print(Format.vprintf("New version is %s", [this._sourceSnapshotPath.get_path()]))
+	if (!this._buildNeeded)
+	    this._buildNeeded = changed;
+	this._runBuild();
+	this._runResolve();
 	this._updateStatus();
     },
-    
-    _run_build: function() {
-	let cancellable = null;
-	if (this._build_taskset.isRunning()) throw new Error();
-	if (!this._build_needed) throw new Error();
 
-	this._build_needed = false;
-
-	let snapshotName = this._source_snapshot_path.get_basename();
-
-	let workdir = this._build_taskset.prepare();
-	let tmpSnapshotPath = workdir.get_child(snapshotName);
-	GSystem.file_linkcopy(this._source_snapshot_path, tmpSnapshotPath,
-			      Gio.FileCopyFlags.OVERWRITE, cancellable);	
-
-	let version = this._src_db.parseVersionStr(this._source_snapshot_path.get_basename());
-	let meta = {'version': version,
-		    'version-path': this._snapshot_dir.get_relative_path(this._source_snapshot_path)};
-	let metaPath = workdir.get_child('meta.json');
-	JsonUtil.writeJsonFileAtomic(metaPath, meta, cancellable);
-	
-	let args = ['ostbuild', 'build', '--snapshot=' + snapshotName];
-
-	let context = new GSystem.SubprocessContext({ argv: args });
-	let task = this._build_taskset.start(context,
-					     cancellable,
-					     Lang.bind(this, this._onBuildExited));
-	print(Format.vprintf("Build task %s started", [task.versionstr]));
-
-	this._updateStatus();
+    _onBuildExited: function(buildTaskset, success, msg) {
+       print(Format.vprintf("build exited; success=%s msg=%s", [success, msg]))
+       if (this._buildNeeded)
+           this._runBuild()
+       
+       this._updateStatus();
     },
-
-    _run_builddisks: function() {
+    
+    _runBuild: function() {
 	let cancellable = null;
-
-	if (!this._do_builddisks || this._builddisks_taskset.isRunning())
+	if (this._taskmaster.isTaskQueued(this._buildTaskName))
 	    return;
-
-	let args = ['ostbuild', 'build-disks'];
-
-	let context = new GSystem.SubprocessContext({ argv: args });
-	let task = this._builddisks_taskset.start(context,
-						  cancellable,
-						  Lang.bind(this, this._onBuildDisksExited));
-	print(Format.vprintf("Builddisks task %s started", [task.versionstr]));
-
-	this._updateStatus();
-    },
-
-    _run_smoke: function() {
-	let cancellable = null;
-
-	if (!this._do_smoke || this._smoke_taskset.isRunning())
+	if (!this._buildNeeded)
 	    return;
 
-	let args = ['ostbuild', 'qa-smoketest'];
-
-	let context = new GSystem.SubprocessContext({ argv: args });
-	let task = this._smoke_taskset.start(context,
-					     cancellable,
-					     Lang.bind(this, this._onSmokeExited));
-	print(Format.vprintf("Smoke task %s started", [task.versionstr]));
-
+	this._buildNeeded = false;
+	this._taskmaster.pushTask(this._buildTaskName);
 	this._updateStatus();
     },
 
-    _onBuildExited: function(buildTaskset, success, msg) {
-	print(Format.vprintf("build exited; success=%s msg=%s", [success, msg]))
-	if (this._build_needed)
-	    this._run_build()
-	if (success)
-	    this._run_builddisks();
-	
-	this._updateStatus();
-    },
-
-    _onBuildDisksExited: function(buildTaskset, success, msg) {
-	print(Format.vprintf("builddisks exited; success=%s msg=%s", [success, msg]))
-	this._updateStatus();
-
-	if (success)
-	    this._run_smoke();
+    _runBdiff: function() {
+	if (this._taskmaster.isTaskQueued(this._bdiffTaskName))
+	    return;
 
+	this._taskmaster.pushTask(this._bdiffTaskName);
 	this._updateStatus();
-    },
-
-    _getBuildDiffForTask: function(task) {
-	let cancellable = null;
-        if (task.build_diff != undefined)
-            return task.build_diff;
-        let metaPath = task.path.get_child('meta.json');
-	if (!metaPath.query_exists(null)) {
-	    task.build_diff = null;
-	    return task.build_diff;
-	}
-	let meta = JsonUtil.loadJson(metaPath, cancellable);
-        let snapshotPath = this._snapshot_dir.get_child(meta['version-path']);
-        let prevSnapshotPath = this._src_db.getPreviousPath(snapshotPath);
-        if (prevSnapshotPath == null) {
-            task.build_diff = null;
-        } else {
-            task.build_diff = Snapshot.snapshotDiff(this._src_db.loadFromPath(snapshotPath, cancellable),
-                                                    this._src_db.loadFromPath(prevSnapshotPath, cancellable));
-	}
-	return task.build_diff;
-    },
-
-    _buildHistoryToJson: function() {
-	let cancellable = null;
-        let history = this._build_taskset.getHistory();
-	let l = history.length;
-        let MAXITEMS = 5;
-        let entries = [];
-	for (let i = Math.max(l - MAXITEMS, 0); i >= 0 && i < l; i++) {
-	    let item = history[i];
-            let data = {v: item.versionstr,
-			state: item.state,
-			timestamp: item.timestamp};
-            entries.push(data);
-            let metaPath = item.path.get_child('meta.json');
-            if (metaPath.query_exists(cancellable)) {
-		data['meta'] = JsonUtil.loadJson(metaPath, cancellable);
-	    }
-            data['diff'] = this._getBuildDiffForTask(item);
-	}
-	return entries;
-    },
-
-    _writeStatusFile: function() {
-	let cancellable = null;
-        let status = {'prefix': this.prefix};
-        if (this._source_snapshot_path != null) {
-            let version = this._src_db.parseVersionStr(this._source_snapshot_path.get_basename());
-            status['version'] = version;
-            status['version-path'] = this._snapshot_dir.get_relative_path(this._source_snapshot_path);
-        } else {
-            status['version'] = '';
-	}
-        
-        status['build'] = this._buildHistoryToJson();
-        
-        if (this._build_proc != null) {
-	    let buildHistory = this._build_taskset.getHistory();
-            let activeBuild = buildHistory[buildHistory.length-1];
-	    let buildStatus = status['build'];
-	    let activeBuildJson = buildStatus[buildStatus.length-1];
-            let statusPath = activeBuild.path.get_child('status.json');
-            if (statusPath.query_exists(null)) {
-                activeBuildJson['build-status'] = JsonUtil.loadJson(statusPath);
-	    }
-	}
-	
-	JsonUtil.writeJsonFileAtomic(this._status_path, status, cancellable);
     }
 });
diff --git a/src/ostbuild/js/builtins/git_mirror.js b/src/ostbuild/js/builtins/git_mirror.js
index b1101c3..d1c05cb 100644
--- a/src/ostbuild/js/builtins/git_mirror.js
+++ b/src/ostbuild/js/builtins/git_mirror.js
@@ -41,6 +41,7 @@ const GitMirror = new Lang.Class({
         this.parser.addArgument('--prefix');
         this.parser.addArgument('--manifest');
         this.parser.addArgument('--snapshot');
+        this.parser.addArgument('--timeout-sec', { help: "Cache fetch results for provided number of seconds" });
         this.parser.addArgument('--fetch', {action:'storeTrue',
 				       help:"Also do a git fetch for components"});
         this.parser.addArgument(['-k', '--keep-going'], {action:'storeTrue',
@@ -51,17 +52,13 @@ const GitMirror = new Lang.Class({
     execute: function(args, loop, cancellable) {
         let parser = new ArgParse.ArgumentParser();
 
+	if (!args.timeout_sec)
+	    args.timeout_sec = 0;
+
         if (args.manifest != null) {
-            let snapshotData = JsonUtil.loadJson(Gio.File.new_for_path(args.manifest), cancellable);
-	    let resolvedComponents = [];
-	    let components = snapshotData['components'];
-	    for (let i = 0; i < components.length; i++) {
-		resolvedComponents.push(BuildUtil.resolveComponent(snapshotData, components[i]));
-	    }
-            snapshotData['components'] = resolvedComponents;
-            snapshotData['patches'] = BuildUtil.resolveComponent(snapshotData, snapshotData['patches']);
-            snapshotData['base'] = BuildUtil.resolveComponent(snapshotData, snapshotData['base']);
-	    this._snapshot = new Snapshot.Snapshot(snapshotData, null);
+	    let manifestPath = Gio.File.new_for_path(args.manifest)
+            let manifestData = JsonUtil.loadJson(manifestPath, cancellable);
+	    this._snapshot = new Snapshot.Snapshot(manifestData, manifestPath, { prepareResolve: true });
         } else {
 	    this._initSnapshot(args.prefix, args.snapshot, cancellable);
 	}
@@ -74,18 +71,15 @@ const GitMirror = new Lang.Class({
 	}
 
 	componentNames.forEach(Lang.bind(this, function (name) {
-            let component = this._snapshot.getComponent(name);
-            let src = component['src']
-            let [keytype, uri] = Vcs.parseSrcKey(src);
-            let branch = component['branch'];
-            let tag = component['tag'];
-            let branchOrTag = branch || tag;
+	    let [keytype, uri, branchOrTag] = this._snapshot.getVcsInfo(name);
 
             if (!args.fetch) {
                 Vcs.ensureVcsMirror(this.mirrordir, keytype, uri, branchOrTag, cancellable);
 	    } else {
 		print("Running git fetch for " + name);
-		Vcs.fetch(this.mirrordir, keytype, uri, branchOrTag, cancellable, {keepGoing:args.keep_going});
+		Vcs.fetch(this.mirrordir, keytype, uri, branchOrTag, cancellable,
+			  { keepGoing:args.keep_going,
+			    timeoutSec: args.timeout_sec });
 	    }
 	}));
     }
diff --git a/src/ostbuild/js/builtins/make.js b/src/ostbuild/js/builtins/make.js
new file mode 100644
index 0000000..cab5084
--- /dev/null
+++ b/src/ostbuild/js/builtins/make.js
@@ -0,0 +1,86 @@
+// Copyright (C) 2012,2013 Colin Walters <walters verbum org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const Format = imports.format;
+
+const GSystem = imports.gi.GSystem;
+
+const Builtin = imports.builtin;
+const Task = imports.task;
+const JsonDB = imports.jsondb;
+const ProcUtil = imports.procutil;
+const JsonUtil = imports.jsonutil;
+const Snapshot = imports.snapshot;
+const Config = imports.config;
+const BuildUtil = imports.buildutil;
+const Vcs = imports.vcs;
+const ArgParse = imports.argparse;
+
+const Make = new Lang.Class({
+    Name: 'Make',
+    Extends: Builtin.Builtin,
+
+    DESCRIPTION: "Execute tasks",
+
+    _init: function() {
+	this.parent();
+	this.parser.addArgument('taskname');
+	this.parser.addArgument('parameters', { nargs: '*' });
+    },
+
+    execute: function(args, loop, cancellable) {
+	this._loop = loop;
+	this._failed = false;
+	let taskmaster = new Task.TaskMaster(this.workdir.get_child('tasks'),
+					     { onEmpty: Lang.bind(this, this._onTasksComplete) });
+	this._taskmaster = taskmaster;
+	taskmaster.connect('task-complete', Lang.bind(this, this._onTaskCompleted));
+	let params = {};
+	for (let i = 0; i < args.parameters.length; i++) { 
+	    let param = args.parameters[i];
+	    let idx = param.indexOf('=');
+	    if (idx == -1)
+		throw new Error("Invalid key=value syntax");
+	    let k = param.substr(0, idx);
+	    let v = JSON.parse(param.substr(idx+1));
+	    params[k] = v;
+	}
+	taskmaster.pushTask(args.taskname, params);
+	this._console = GSystem.Console.get();
+	loop.run();
+	if (!this._failed)
+	    print("Success!")
+    },
+
+    _onTaskCompleted: function(taskmaster, task, success, error) {
+	if (success) {
+	    print("Task " + task.name + " complete: " + task._workdir.get_path());
+	} else {
+	    this._failed = true;
+	    print("Task " + task.name + " failed: " + task._workdir.get_path());
+	}
+    },
+
+    _onTasksComplete: function(success, err) {
+	if (!success)
+	    this._err = err;
+	this._loop.quit();
+    }
+});
diff --git a/src/ostbuild/js/builtins/run_task.js b/src/ostbuild/js/builtins/run_task.js
new file mode 100644
index 0000000..4a34c32
--- /dev/null
+++ b/src/ostbuild/js/builtins/run_task.js
@@ -0,0 +1,55 @@
+// Copyright (C) 2012,2013 Colin Walters <walters verbum org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const Format = imports.format;
+
+const GSystem = imports.gi.GSystem;
+
+const Builtin = imports.builtin;
+const Task = imports.task;
+const JsonDB = imports.jsondb;
+const ProcUtil = imports.procutil;
+const JsonUtil = imports.jsonutil;
+const Snapshot = imports.snapshot;
+const Config = imports.config;
+const BuildUtil = imports.buildutil;
+const Vcs = imports.vcs;
+const ArgParse = imports.argparse;
+
+const RunTask = new Lang.Class({
+    Name: 'RunTask',
+    Extends: Builtin.Builtin,
+
+    DESCRIPTION: "Internal helper to execute a task",
+
+    _init: function() {
+	this.parent();
+	this.parser.addArgument('taskName');
+	this.parser.addArgument('parameters');
+    },
+
+    execute: function(args, loop, cancellable) {
+	let taskset = Task.TaskSet.prototype.getInstance();
+	let [taskDef, vars] = taskset.getTask(args.taskName);
+	let params = JSON.parse(args.parameters);
+	let instance = new taskDef(null, args.taskName, vars, params);
+	instance.execute(cancellable);
+    }
+});
diff --git a/src/ostbuild/js/jsondb.js b/src/ostbuild/js/jsondb.js
index c61b311..d1d4b8d 100644
--- a/src/ostbuild/js/jsondb.js
+++ b/src/ostbuild/js/jsondb.js
@@ -99,6 +99,16 @@ const JsonDB = new Lang.Class({
 	return JsonUtil.loadJson(this._path.get_child(path.get_basename()), cancellable);
     },
 
+    _updateIndex: function(cancellable) {
+        let files = this._getAll();
+	let fnames = [];
+	for (let i = 0; i < files.length; i++) {
+	    fnames.push(files[i][3]);
+	}
+	let index = { files: fnames };
+	JsonUtil.writeJsonFileAtomic(this._path.get_child('index.json'), index, cancellable);
+    },
+
     store: function(obj, cancellable) {
         let files = this._getAll();
 	let latest = null;
@@ -133,6 +143,8 @@ const JsonDB = new Lang.Class({
 	    }
 	}
 
+	this._updateIndex(cancellable);
+
         return [targetPath, true];
     }
 });
diff --git a/src/ostbuild/js/jsonutil.js b/src/ostbuild/js/jsonutil.js
index 04a82ca..24deb88 100644
--- a/src/ostbuild/js/jsonutil.js
+++ b/src/ostbuild/js/jsonutil.js
@@ -27,6 +27,44 @@ function writeJsonToStream(stream, data, cancellable) {
     stream.write_bytes(new GLib.Bytes(buf), cancellable);
 }
 
+function writeJsonToStreamAsync(stream, data, cancellable, onComplete) {
+    let buf = JSON.stringify(data, null, "  ");
+    stream.write_bytes_async(new GLib.Bytes(buf), GLib.PRIORITY_DEFAULT,
+			     cancellable, function(stream, result) {
+				 let err = null;
+				 try {
+				     stream.write_bytes_finish(result);
+				 } catch (e) {
+				     err = e;
+				 } 
+				 onComplete(err != null, err);
+			     });
+}
+
+function loadJsonFromStream(stream, cancellable) {
+    let membuf = Gio.MemoryOutputStream.new_resizable();
+    membuf.splice(stream, Gio.OutputStreamSpliceFlags.CLOSE_TARGET, cancellable);
+    let bytes = membuf.steal_as_bytes();
+    return JSON.parse(bytes.toArray().toString());
+}
+
+function loadJsonFromStreamAsync(stream, cancellable, onComplete) {
+    let membuf = Gio.MemoryOutputStream.new_resizable();
+    membuf.splice_async(stream, Gio.OutputStreamSpliceFlags.CLOSE_TARGET, GLib.PRIORITY_DEFAULT,
+			cancellable, function(stream, result) {
+			    let err = null;
+			    let res = null;
+			    try {
+				stream.splice_finish(result);
+				let bytes = membuf.steal_as_bytes();
+				res = JSON.parse(bytes.toArray().toString());
+			    } catch (e) {
+				err = e;
+			    }
+			    onComplete(res, err);
+			});
+}
+
 function writeJsonFileAtomic(path, data, cancellable) {
     let s = path.replace(null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
     writeJsonToStream(s, data, cancellable);
diff --git a/src/ostbuild/js/jsutil.js b/src/ostbuild/js/jsutil.js
new file mode 100644
index 0000000..7b37a37
--- /dev/null
+++ b/src/ostbuild/js/jsutil.js
@@ -0,0 +1,23 @@
+// Copyright (C) 2013 Colin Walters <walters verbum org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+function stringEndswith(s, suffix) {
+    let i = s.lastIndexOf(suffix);
+    if (i == -1)
+	return false;
+    return i == s.length - suffix.length;
+}
diff --git a/src/ostbuild/js/main.js b/src/ostbuild/js/main.js
index ae92800..0f1e2e2 100755
--- a/src/ostbuild/js/main.js
+++ b/src/ostbuild/js/main.js
@@ -26,7 +26,9 @@ const BUILTINS = ['autobuilder',
                   'resolve',
                   'build',
                   'build-disks',
+                  'make',
                   'shell',
+                  'run-task',
                   'qa-make-disk',
 		  'qa-pull-deploy',
 		  'qa-smoketest'];
diff --git a/src/ostbuild/js/procutil.js b/src/ostbuild/js/procutil.js
index 0af1ab3..3c30ab3 100644
--- a/src/ostbuild/js/procutil.js
+++ b/src/ostbuild/js/procutil.js
@@ -92,7 +92,7 @@ function _runSyncGetOutputInternal(argv, cancellable, params, splitLines) {
 	    let [line, len] = dataIn.read_line_utf8(cancellable);
 	    if (line == null)
 		break;
-	    result += line;
+	    result += (line + '\n');
 	}
     }
     _wait_sync_check_internal(proc, cancellable);
diff --git a/src/ostbuild/js/snapshot.js b/src/ostbuild/js/snapshot.js
index 51dca5f..fbe760c 100644
--- a/src/ostbuild/js/snapshot.js
+++ b/src/ostbuild/js/snapshot.js
@@ -21,14 +21,17 @@ const Lang = imports.lang;
 
 const JsonDB = imports.jsondb;
 const JsonUtil = imports.jsonutil;
+const Vcs = imports.vcs;
 const Params = imports.params;
 
 function _componentDict(snapshot) {
     let r = {};
     let components = snapshot['components'];
-    for (let i = 0; i< components.length; i++) {
+    for (let i = 0; i < components.length; i++) {
 	let component = components[i];
 	let name = component['name'];
+	if (r[name])
+            throw new Error("Duplicate component name " + name);
         r[name] = component;
     }
     let patches = snapshot['patches'];
@@ -66,15 +69,69 @@ function snapshotDiff(a, b) {
 const Snapshot = new Lang.Class({
     Name: 'Snapshot',
     
-    _init: function(data, path) {
+    _init: function(data, path, params) {
+	params = Params.parse(params, { prepareResolve: false });
 	this.data = data;
 	this.path = path;
+	if (params.prepareResolve) {
+	    data['patches'] = this._resolveComponent(data, data['patches']);
+	    data['base'] = this._resolveComponent(data, data['base']);
+	    for (let i = 0; i < data['components'].length; i++) {
+		let component = this._resolveComponent(data, data['components'][i]);
+		data['components'][i] = component;
+	    }
+	}
 	this._componentDict = _componentDict(data);
 	this._componentNames = [];
 	for (let k in this._componentDict)
 	    this._componentNames.push(k);
     },
 
+    _resolveComponent: function(manifest, componentMeta) {
+	let result = {};
+	Lang.copyProperties(componentMeta, result);
+	let origSrc = componentMeta['src'];
+
+	let didExpand = false;
+	let vcsConfig = manifest['vcsconfig'];
+	for (let vcsprefix in vcsConfig) {
+	    let expansion = vcsConfig[vcsprefix];
+            let prefix = vcsprefix + ':';
+            if (origSrc.indexOf(prefix) == 0) {
+		result['src'] = expansion + origSrc.substr(prefix.length);
+		didExpand = true;
+		break;
+	    }
+	}
+
+	let name = componentMeta['name'];
+	let src, idx, name;
+	if (name == undefined) {
+            if (didExpand) {
+		src = origSrc;
+		idx = src.lastIndexOf(':');
+		name = src.substr(idx+1);
+            } else {
+		src = result['src'];
+		idx = src.lastIndexOf('/');
+		name = src.substr(idx+1);
+	    }
+	    let i = name.lastIndexOf('.git');
+            if (i != -1 && i == name.length - 4) {
+		name = name.substr(0, name.length - 4);
+	    }
+            name = name.replace(/\//g, '-');
+            result['name'] = name;
+	}
+
+	let branchOrTag = result['branch'] || result['tag'];
+	if (!branchOrTag) {
+            result['branch'] = 'master';
+	}
+
+	return result;
+    },
+
     _expandComponent: function(component) {
 	let r = {};
 	Lang.copyProperties(component, r);
@@ -98,6 +155,10 @@ const Snapshot = new Lang.Class({
 	return this._componentNames;
     },
 
+    getComponentMap: function() {
+	return this._componentDict;
+    },
+
     getComponent: function(name, allowNone) {
 	let r = this._componentDict[name] || null;
 	if (!r && !allowNone)
@@ -118,5 +179,15 @@ const Snapshot = new Lang.Class({
 
     getExpanded: function(name) {
 	return this._expandComponent(this.getComponent(name));
+    },
+
+    getVcsInfo: function(name) {
+	let component = this.getComponent(name);
+        let src = component['src']
+        let [keytype, uri] = Vcs.parseSrcKey(src);
+        let branch = component['branch'];
+        let tag = component['tag'];
+        let branchOrTag = branch || tag;
+	return [keytype, uri, branchOrTag];
     }
 });
diff --git a/src/ostbuild/js/task.js b/src/ostbuild/js/task.js
new file mode 100644
index 0000000..0f6c3a6
--- /dev/null
+++ b/src/ostbuild/js/task.js
@@ -0,0 +1,381 @@
+// Copyright (C) 2012,2013 Colin Walters <walters verbum org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const format = imports.format;
+const Lang = imports.lang;
+const Signals = imports.signals;
+
+const GSystem = imports.gi.GSystem;
+const Config = imports.config;
+const Params = imports.params;
+const JsonUtil = imports.jsonutil;
+const JsonDB = imports.jsondb;
+const ProcUtil = imports.procutil;
+const BuildUtil = imports.buildutil;
+
+var _tasksetInstance = null;
+const TaskSet = new Lang.Class({
+    Name: 'TaskSet',
+    
+    _init: function() {
+	this._tasks = [];
+	let taskdir = Gio.File.new_for_path(GLib.getenv('OSTBUILD_DATADIR')).resolve_relative_path('js/tasks');
+	let denum = taskdir.enumerate_children('standard::*', 0, null);
+	let finfo;
+	
+	for (let taskmodname in imports.tasks) {
+	    let taskMod = imports.tasks[taskmodname];
+	    for (let defname in taskMod) {
+		if (defname.indexOf('Task') !== 0
+		    || defname == 'Task')
+		    continue;
+		let cls = taskMod[defname];
+		this.register(cls);
+	    }
+	}
+    },
+
+    register: function(taskdef) {
+	this._tasks.push(taskdef);
+    },
+
+    getAllTasks: function() {
+	return this._tasks;
+    },
+
+    getTask: function(taskName, params) {
+	params = Params.parse(params, { allowNone: false })
+	for (let i = 0; i < this._tasks.length; i++) {
+	    let taskDef = this._tasks[i];
+            let pattern = taskDef.prototype.TaskPattern;
+            let re = pattern[0];
+            let match = re.exec(taskName);
+            if (!match)
+		continue;
+            let vars = {};
+            for (let i = 1; i < pattern.length; i++) {
+		vars[pattern[i]] = match[i];
+            }
+	    return [taskDef, vars];
+	}
+	if (!params.allowNone)
+	    throw new Error("No task definition matches " + taskName);
+	return null;
+    },
+
+    getInstance: function() {
+	if (!_tasksetInstance)
+	    _tasksetInstance = new TaskSet();
+	return _tasksetInstance;
+    }
+});
+    
+const TaskMaster = new Lang.Class({
+    Name: 'TaskMaster',
+
+    _init: function(path, params) {
+	params = Params.parse(params, {onEmpty: null});
+	this.path = path;
+	this.maxConcurrent = GLib.get_num_processors();
+	this._onEmpty = params.onEmpty;
+	this.cancellable = null;
+	this._idleRecalculateId = 0;
+	this._executing = [];
+	this._pendingTasksList = [];
+	this._seenTasks = {};
+	this._taskErrors = {};
+	this._caughtError = false;
+
+	this._taskset = TaskSet.prototype.getInstance();
+    },
+
+    pushTask: function(taskName, parameters) {
+	if (this.isTaskQueued(taskName))
+	    return;
+	let [taskDef, vars] = this._taskset.getTask(taskName);
+	let instance = new taskDef(this, taskName, vars, parameters);
+	instance.onComplete = Lang.bind(this, this._onComplete, instance);
+	this._pendingTasksList.push(instance);
+	this._queueRecalculate();
+    },
+
+    isTaskQueued: function(taskName) {
+	for (let i = 0; i < this._pendingTasksList.length; i++) {
+	    let pending = this._pendingTasksList[i];
+	    if (pending.TaskName == taskName)
+		return true;
+	}
+	for (let i = 0; i < this._executing.length; i++) {
+	    let executingTask = this._executing[i];
+	    if (executingTask.TaskName == taskName)
+		return true;
+	}
+	return false;
+    },
+
+    getTaskState: function() {
+	let r = [];
+	for (let i = 0; i < this._pendingTasksList.length; i++) {
+	    r.push({running: false, task: this._pendingTasksList[i] });
+	}
+	for (let i = 0; i < this._executing.length; i++) {
+	    r.push({running: true, task: this._executing[i] });
+	}
+	return r;
+    },
+
+    _queueRecalculate: function() {
+	if (this._idleRecalculateId > 0)
+	    return;
+	this._idleRecalculateId = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, this._recalculate));
+    },
+
+    _recalculate: function() {
+	this._idleRecalculateId = 0;
+
+	if (this._executing.length == 0 &&
+	    this._pendingTasksList.length == 0) {
+	    this._onEmpty(true, null);
+	    return;
+	} else if (this._pendingTasksList.length == 0) {
+	    return;
+	}
+
+	this._reschedule();
+    },
+
+    _onComplete: function(success, error, task) {
+	this.emit('task-complete', task, success, error);
+	let idx = -1;
+	for (let i = 0; i < this._executing.length; i++) {
+	    let executingTask = this._executing[i];
+	    if (executingTask !== task)
+		continue;
+	    idx = i;
+	    break;
+	}
+	if (idx == -1)
+	    throw new Error("TaskMaster: Internal error - Failed to find completed task:" + task.TaskName);
+	this._executing.splice(idx, 1);
+	this._queueRecalculate();
+    },
+
+    _reschedule: function() {
+	while (this._executing.length < this.maxConcurrent &&
+	       this._pendingTasksList.length > 0) {
+	    let task = this._pendingTasksList.shift();
+	    task._executeInSubprocessInternal(this.cancellable);
+	    this._executing.push(task);
+	}
+    }
+});
+Signals.addSignalMethods(TaskMaster.prototype);
+
+const TaskDef = new Lang.Class({
+    Name: 'TaskDef',
+
+    TaskPattern: null,
+
+    PreserveStdout: true,
+    RetainFailed: 1,
+    RetainSuccess: 5,
+
+    DefaultParameters: {},
+
+    _VERSION_RE: /^(\d+\d\d\d\d)\.(\d+)$/,
+
+    _init: function(taskmaster, name, vars, parameters) {
+	this.taskmaster = taskmaster;
+	this.name = name;
+	this.vars = vars;
+	this.parameters = Params.parse(parameters, this.DefaultParameters);
+
+	this.config = Config.get();
+	this.workdir = Gio.File.new_for_path(this.config.getGlobal('workdir'));
+	this.resultdir = this.workdir.get_child('results');
+	this.mirrordir = Gio.File.new_for_path(this.config.getGlobal('mirrordir'));
+	this.libdir = Gio.File.new_for_path(GLib.getenv('OSTBUILD_LIBDIR'));
+	this.repo = this.workdir.get_child('repo');
+    },
+
+    getDepends: function() {
+	return [];
+    },
+
+    _getResultDb: function(taskname) {
+	let path = this.resultdir.resolve_relative_path(taskname);
+	return new JsonDB.JsonDB(path);
+    },
+
+    _loadVersionsFrom: function(dir, cancellable) {
+	let e = dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
+	let info;
+	let results = [];
+	while ((info = e.next_file(cancellable)) != null) {
+	    let name = info.get_name();
+	    let match = this._VERSION_RE.exec(name);
+	    if (!match)
+		continue;
+	    results.push(name);
+	}
+	results.sort(BuildUtil.compareVersions);
+	return results;
+    },
+
+    _cleanOldVersions: function(dir, retain, cancellable) {
+	let versions = this._loadVersionsFrom(dir, cancellable);
+	while (versions.length > retain) {
+	    let child = dir.get_child(versions.shift());
+	    GSystem.shutil_rm_rf(child, cancellable);
+	}
+    },
+
+    execute: function(cancellable) {
+	throw new Error("Not implemented");
+    },
+
+    _loadAllVersions: function(cancellable) {
+	let allVersions = [];
+
+	let successVersions = this._loadVersionsFrom(this._successDir, cancellable);
+	for (let i = 0; i < successVersions.length; i++) {
+	    allVersions.push([true, successVersions[i]]);
+	}
+
+	let failedVersions = this._loadVersionsFrom(this._failedDir, cancellable);
+	for (let i = 0; i < failedVersions.length; i++) {
+	    allVersions.push([false, failedVersions[i]]);
+	}
+
+	allVersions.sort(function (a, b) {
+	    let [successA, versionA] = a;
+	    let [successB, versionB] = b;
+	    return BuildUtil.compareVersions(versionA, versionB);
+	});
+
+	return allVersions;
+    },
+
+    _executeInSubprocessInternal: function(cancellable) {
+	this._cancellable = cancellable;
+
+	this._startTimeMillis = GLib.get_monotonic_time() / 1000;
+
+	this.dir = this.taskmaster.path.resolve_relative_path(this.name);
+	GSystem.file_ensure_directory(this.dir, true, cancellable);
+	
+	this._successDir = this.dir.get_child('successful');
+	GSystem.file_ensure_directory(this._successDir, true, cancellable);
+	this._failedDir = this.dir.get_child('failed');
+	GSystem.file_ensure_directory(this._failedDir, true, cancellable);
+
+	let allVersions = this._loadAllVersions(cancellable);
+
+	let currentTime = GLib.DateTime.new_now_utc();
+
+	let currentYmd = Format.vprintf('%d%02d%02d', [currentTime.get_year(),
+						       currentTime.get_month(),
+						       currentTime.get_day_of_month()]);
+	let version = null;
+	if (allVersions.length > 0) {
+	    let [lastSuccess, lastVersion] = allVersions[allVersions.length-1];
+	    let match = this._VERSION_RE.exec(lastVersion);
+	    if (!match) throw new Error();
+	    let lastYmd = match[1];
+	    let lastSerial = match[2];
+	    if (lastYmd == currentYmd) {
+		version = currentYmd + '.' + (parseInt(lastSerial) + 1);
+	    }
+	}
+	if (version === null) {
+	    version = currentYmd + '.0';
+	}
+
+	this._version = version;
+	this._workdir = this.dir.get_child(version);
+	GSystem.shutil_rm_rf(this._workdir, cancellable);
+	GSystem.file_ensure_directory(this._workdir, true, cancellable);
+
+	let baseArgv = ['ostbuild', 'run-task', this.name, JSON.stringify(this.parameters)];
+	let context = new GSystem.SubprocessContext({ argv: baseArgv });
+	context.set_cwd(this._workdir.get_path());
+	context.set_stdin_disposition(GSystem.SubprocessStreamDisposition.PIPE);
+	if (this.PreserveStdout) {
+	    let outPath = this._workdir.get_child('output.txt');
+	    context.set_stdout_file_path(outPath.get_path());
+	    context.set_stderr_disposition(GSystem.SubprocessStreamDisposition.STDERR_MERGE);
+	} else {
+	    context.set_stdout_disposition(GSystem.SubprocessStreamDisposition.NULL);
+	    let errPath = this._workdir.get_child('errors.txt');
+	    context.set_stderr_file_path(errPath.get_path());
+	}
+	this._proc = new GSystem.Subprocess({ context: context });
+	this._proc.init(cancellable);
+
+	this._proc.wait(cancellable, Lang.bind(this, this._onChildExited));
+    },
+
+    _updateIndex: function(cancellable) {
+	let allVersions = this._loadAllVersions(cancellable);
+
+	let fileList = [];
+	for (let i = 0; i < allVersions.length; i++) {
+	    let [successful, version] = allVersions[i];
+	    let fname = (successful ? 'successful/' : 'failed/') + version;
+	    fileList.push(fname);
+	}
+
+	let index = { files: fileList };
+	JsonUtil.writeJsonFileAtomic(this.dir.get_child('index.json'), index, cancellable);
+    },
+    
+    _onChildExited: function(proc, result) {
+	let cancellable = this._cancellable;
+	let [success, errmsg] = ProcUtil.asyncWaitCheckFinish(proc, result);
+	let target;
+
+	let elapsedMillis = GLib.get_monotonic_time() / 1000 - this._startTimeMillis;
+	let meta = { taskMetaVersion: 0,
+		     taskVersion: this._version,
+		     success: success,
+		     errmsg: errmsg,
+		     elapsedMillis: elapsedMillis };
+	JsonUtil.writeJsonFileAtomic(this._workdir.get_child('meta.json'), meta, cancellable);
+
+	if (!success) {
+	    target = this._failedDir.get_child(this._version);
+	    GSystem.file_rename(this._workdir, target, null);
+	    this._workdir = target;
+	    this._cleanOldVersions(this._failedDir, this.RetainFailed, null);
+	    this.onComplete(success, errmsg);
+	} else {
+	    target = this._successDir.get_child(this._version);
+	    GSystem.file_rename(this._workdir, target, null);
+	    this._workdir = target;
+	    this._cleanOldVersions(this._successDir, this.RetainSuccess, null);
+	    this.onComplete(success, null);
+	}
+	// Also remove any old interrupted versions
+	this._cleanOldVersions(this.dir, 0, null);
+
+	this._updateIndex(cancellable);
+
+	BuildUtil.atomicSymlinkSwap(this.dir.get_child('current'), target, cancellable);
+    }
+});
diff --git a/src/ostbuild/js/tasks/task-bdiff.js b/src/ostbuild/js/tasks/task-bdiff.js
new file mode 100644
index 0000000..1d249f6
--- /dev/null
+++ b/src/ostbuild/js/tasks/task-bdiff.js
@@ -0,0 +1,155 @@
+// Copyright (C) 2011,2012,2013 Colin Walters <walters verbum org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const Format = imports.format;
+
+const GSystem = imports.gi.GSystem;
+
+const Builtin = imports.builtin;
+const Task = imports.task;
+const JsonDB = imports.jsondb;
+const ProcUtil = imports.procutil;
+const StreamUtil = imports.streamutil;
+const JsonUtil = imports.jsonutil;
+const Snapshot = imports.snapshot;
+const Config = imports.config;
+const BuildUtil = imports.buildutil;
+const Vcs = imports.vcs;
+const ArgParse = imports.argparse;
+
+const TaskBdiff = new Lang.Class({
+    Name: "TaskBdiff",
+    Extends: Task.TaskDef,
+
+    TaskPattern: [/bdiff\/(.*?)$/, 'prefix'],
+
+    TaskAfterPrefix: '/build/',
+
+    _gitLogToJson: function(repoDir, specification) {
+	let log = ProcUtil.runSyncGetOutputLines(['git', 'log', '--format=email', specification],
+						 null,
+						 { cwd: repoDir });
+	let r = [];
+	if (log.length == 0)
+	    return r;
+	let currentItem = null;
+	let parsingHeaders = false;
+	let fromRegex = /^From ([0-9a-f]{40}) /;
+	for (let i = 0; i < log.length; i++) {
+	    let line = log[i];
+	    let match = fromRegex.exec(line);
+	    if (match) {
+		if (currentItem !== null) {
+		    r.push(currentItem);
+		}
+		currentItem = {'Checksum': match[1]};
+		parsingHeaders = true;
+	    } else if (parsingHeaders) {
+		if (line.length == 0) {
+		    parsingHeaders = false;
+		} else {
+		    let idx = line.indexOf(':');
+		    let k = line.substr(0, idx);
+		    let v = line.substr(idx+1);
+		    currentItem[k] = v;
+		}
+	    }
+	}
+	return r;
+    },
+
+    _diffstat: function(repoDir, specification) {
+	return ProcUtil.runSyncGetOutputUTF8(['git', 'diff', '--stat', specification], null,
+					     { cwd: repoDir });
+    },
+
+    execute: function(cancellable) {
+	let prefix = this.vars['prefix'];
+
+	this.subworkdir = Gio.File.new_for_path('.');
+
+	let builddb = this._getResultDb('build/' + prefix);
+        let latestPath = builddb.getLatestPath();
+	if (!latestPath)
+	    throw new Error("No builds!")
+        let latestBuildVersion = builddb.parseVersionStr(latestPath.get_basename());
+
+        let previousPath = builddb.getPreviousPath(latestPath);
+	if (!previousPath)
+	    throw new Error("No build previous to " + latestBuildVersion)
+
+        let latestBuildData = builddb.loadFromPath(latestPath, cancellable);
+	let latestBuildSnapshot = new Snapshot.Snapshot(latestBuildData['snapshot'], null);
+        let previousBuildData = builddb.loadFromPath(previousPath, cancellable);
+	let previousBuildSnapshot = new Snapshot.Snapshot(previousBuildData['snapshot'], null);
+
+	let added = [];
+	let modified = [];
+	let removed = [];
+
+	let result = {fromBuildVersion: builddb.parseVersionStr(previousPath.get_basename()),
+		      toBuildVersion: builddb.parseVersionStr(latestPath.get_basename()),
+		      fromSrcVersion: builddb.parseVersionStr(previousBuildData['snapshotName']),
+		      toSrcVersion: builddb.parseVersionStr(latestBuildData['snapshotName']),
+		      added: added,
+		      modified: modified,
+		      removed: removed};
+
+	let modifiedNames = [];
+
+	let latestComponentMap = latestBuildSnapshot.getComponentMap();
+	let previousComponentMap = previousBuildSnapshot.getComponentMap();
+	for (let componentName in latestComponentMap) {
+	    let componentA = latestBuildSnapshot.getComponent(componentName);
+	    let componentB = previousBuildSnapshot.getComponent(componentName, true);
+
+	    if (componentB === null)
+		added.push(componentName);
+	    else if (componentB.revision != componentA.revision)
+		modifiedNames.push(componentName);
+	}
+	for (let componentName in previousComponentMap) {
+	    let componentA = latestBuildSnapshot.getComponent(componentName, true);
+
+	    if (componentA === null)
+		removed.push(componentName);
+	}
+	
+	for (let i = 0; i < modifiedNames.length; i++) {
+	    let componentName = modifiedNames[i];
+	    let latestComponent = latestBuildSnapshot.getComponent(componentName);
+	    let previousComponent = previousBuildSnapshot.getComponent(componentName);
+	    let latestRevision = latestComponent.revision;
+	    let previousRevision = previousComponent.revision;
+	    let [keytype, uri, branchOrTag] = latestBuildSnapshot.getVcsInfo(componentName);
+	    let mirrordir = Vcs.getMirrordir(this.mirrordir, keytype, uri);
+	    
+	    let gitlog = this._gitLogToJson(mirrordir, previousRevision + '...' + latestRevision);
+	    let diffstat = this._diffstat(mirrordir, previousRevision + '..' + latestRevision);
+	    modified.push({ previous: previousComponent,
+			    latest: latestComponent,
+			    gitlog: gitlog,
+			    diffstat: diffstat });
+	}
+
+	let bdiffdb = this._getResultDb('bdiff/' + prefix); 
+	bdiffdb.store(result, cancellable);
+    }
+});
diff --git a/src/ostbuild/js/builtins/build.js b/src/ostbuild/js/tasks/task-build.js
similarity index 92%
rename from src/ostbuild/js/builtins/build.js
rename to src/ostbuild/js/tasks/task-build.js
index 7c54a8a..24f4b80 100644
--- a/src/ostbuild/js/builtins/build.js
+++ b/src/ostbuild/js/tasks/task-build.js
@@ -23,7 +23,7 @@ const Format = imports.format;
 const GSystem = imports.gi.GSystem;
 
 const Builtin = imports.builtin;
-const SubTask = imports.subtask;
+const Task = imports.task;
 const JsonDB = imports.jsondb;
 const ProcUtil = imports.procutil;
 const StreamUtil = imports.streamutil;
@@ -35,13 +35,15 @@ const Vcs = imports.vcs;
 const ArgParse = imports.argparse;
 
 const OPT_COMMON_CFLAGS = {'i686': '-O2 -g -m32 -march=i686 -mtune=atom -fasynchronous-unwind-tables',
-                           'x86_64': '-O2 -g -m64 -mtune=generic'}
+                           'x86_64': '-O2 -g -m64 -mtune=generic'};
 
-const Build = new Lang.Class({
-    Name: "Build",
-    Extends: Builtin.Builtin,
+const TaskBuild = new Lang.Class({
+    Name: "TaskBuild",
+    Extends: Task.TaskDef,
 
-    DESCRIPTION: "Build multiple components and generate trees",
+    TaskPattern: [/build\/(.*?)$/, 'prefix'],
+
+    TaskAfterPrefix: '/resolve/',
 
     _resolveRefs: function(refs) {
         if (refs.length == 0)
@@ -290,6 +292,7 @@ const Build = new Lang.Class({
 
 	let prefix = this._snapshot.data['prefix'];
         let buildname = Format.vprintf('%s/%s/%s', [prefix, basename, architecture]);
+        let unixBuildname = buildname.replace(/\//g, '_');
         let buildRef = 'components/' + buildname;
 
         let currentVcsVersion = component['revision'];
@@ -320,16 +323,13 @@ const Build = new Lang.Class({
 	let patchdir;
         if (expandedComponent['patches']) {
             let patchesRevision = expandedComponent['patches']['revision'];
-            if (this.args.patches_path) {
-                patchdir = Gio.File.new_for_path(this.args.patches_path);
-            } else if (this._cachedPatchdirRevision == patchesRevision) {
+            if (this._cachedPatchdirRevision == patchesRevision) {
                 patchdir = this.patchdir;
             } else {
                 patchdir = Vcs.checkoutPatches(this.mirrordir,
                                                this.patchdir,
                                                expandedComponent,
-					       cancellable,
-                                               {patchesPath: this.args.patches_path});
+					       cancellable);
                 this._cachedPatchdirRevision = patchesRevision;
 	    }
             if ((previousMetadata != null) &&
@@ -366,33 +366,31 @@ const Build = new Lang.Class({
 	    }
 	}
 
-        let taskdir = this.workdir.get_child('tasks');
-        let buildTaskset = new SubTask.TaskSet(taskdir.get_child(buildname));
-
-	let workdir = buildTaskset.prepare();
+	let cwd = Gio.File.new_for_path('.');
+	let buildWorkdir = cwd.get_child('tmp-' + unixBuildname);
+	GSystem.file_ensure_directory(buildWorkdir, true, cancellable);
 
-        let tempMetadataPath = workdir.get_child('_ostbuild-meta.json');
+        let tempMetadataPath = buildWorkdir.get_child('_ostbuild-meta.json');
         JsonUtil.writeJsonFileAtomic(tempMetadataPath, expandedComponent, cancellable);
 
-        let componentSrc = workdir.get_child(basename);
+        let componentSrc = buildWorkdir.get_child(basename);
         let childArgs = ['ostbuild', 'checkout', '--snapshot=' + this._snapshot.path.get_path(),
 			 '--checkoutdir=' + componentSrc.get_path(),
 			 '--metadata-path=' + tempMetadataPath.get_path(),
 			 '--overwrite', basename];
-        if (this.args.patches_path)
-            childArgs.push('--patches-path=' + this.args.patches_path);
-        else if (patchdir)
+        if (patchdir) {
             childArgs.push('--patches-path=' + patchdir.get_path());
-        ProcUtil.runSync(childArgs, cancellable, { logInitiation: true });
+	}
+        ProcUtil.runSync(childArgs, cancellable);
 
         GSystem.file_unlink(tempMetadataPath, cancellable);
 
-        let componentResultdir = workdir.get_child('results');
+        let componentResultdir = buildWorkdir.get_child('results');
         GSystem.file_ensure_directory(componentResultdir, true, cancellable);
 
-        let rootdir = this._composeBuildroot(workdir, basename, architecture, cancellable);
+        let rootdir = this._composeBuildroot(buildWorkdir, basename, architecture, cancellable);
 
-        let tmpdir=workdir.get_child('tmp');
+        let tmpdir=buildWorkdir.get_child('tmp');
         GSystem.file_ensure_directory(tmpdir, true, cancellable);
 
         let srcCompileOnePath = this.libdir.get_child('ostree-build-compile-one');
@@ -424,26 +422,11 @@ const Build = new Lang.Class({
 
 	let context = new GSystem.SubprocessContext({ argv: childArgs });
 	context.set_environment(ProcUtil.objectToEnvironment(envCopy));
-
-	let mainContext = new GLib.MainContext();
-	mainContext.push_thread_default();
-	let loop = GLib.MainLoop.new(mainContext, true);
-	let t;
-	try {
-	    t = buildTaskset.start(context, cancellable, Lang.bind(this, this._onBuildComplete, loop));
-	    print("Started child process " + context.argv.map(GLib.shell_quote).join(' '));
-	    loop.run();
-	} finally {
-	    mainContext.pop_thread_default();
-	}
-	let buildSuccess = this._currentBuildSucceded;
-	let msg = this._currentBuildSuccessMsg;
-
-        if (!buildSuccess) {
-            this._analyzeBuildFailure(t, architecture, component, componentSrc,
-                                      currentVcsVersion, previousVcsVersion, cancellable);
-	    throw new Error("Build failure in component " + buildname + " : " + msg);
-	}
+	
+	let proc = new GSystem.Subprocess({ context: context });
+	proc.init(cancellable);
+	print("Started child process " + context.argv.map(GLib.shell_quote).join(' '));
+	proc.wait_sync_check(cancellable);
 
         let recordedMetaPath = componentResultdir.get_child('_ostbuild-meta.json');
         JsonUtil.writeJsonFileAtomic(recordedMetaPath, expandedComponent, cancellable);
@@ -472,7 +455,7 @@ const Build = new Lang.Class({
         if (statoverridePath != null)
             GSystem.file_unlink(statoverridePath, cancellable);
 
-        GSystem.shutil_rm_rf(tmpdir, cancellable);
+        GSystem.shutil_rm_rf(buildWorkdir, cancellable);
 
         let ostreeRevision = this._saveComponentBuild(buildname, expandedComponent, cancellable);
 
@@ -485,8 +468,7 @@ const Build = new Lang.Class({
         let runtimeName = 'bases/' + base['runtime'];
         let develName = 'bases/' + base['devel'];
 
-	let rootdir = this.workdir.get_child('roots');
-        let composeRootdir = rootdir.get_child(target['name']);
+        let composeRootdir = this.subworkdir.get_child(target['name']);
 	GSystem.shutil_rm_rf(composeRootdir, cancellable);
         GSystem.file_ensure_directory(composeRootdir, true, cancellable);
 
@@ -623,8 +605,7 @@ const Build = new Lang.Class({
 	Lang.copyProperties(BuildUtil.BUILD_ENV, env);
         env['DL_DIR'] = downloads.get_path();
         env['SSTATE_DIR'] = sstateDir.get_path();
-        ProcUtil.runSync(cmd, cancellable, { logInitiation: true,
-					     env:ProcUtil.objectToEnvironment(env) });
+        ProcUtil.runSync(cmd, cancellable, {env:ProcUtil.objectToEnvironment(env)});
 
 	let componentTypes = ['runtime', 'devel'];
         for (let i = 0; i < componentTypes.length; i++) {
@@ -642,26 +623,30 @@ const Build = new Lang.Class({
 	builtRevisionPath.replace_contents(basemeta['revision'], null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
     },
 
-    _init: function() {
-	this.parent();
-        this.parser.addArgument('--prefix');
-        this.parser.addArgument('--snapshot');
-        this.parser.addArgument('--patches-path');
+    execute: function(cancellable) {
+	let prefix = this.vars['prefix'];
+
+	this.subworkdir = Gio.File.new_for_path('.');
 
         this.forceBuildComponents = {};
         this.cachedPatchdirRevision = null;
-    },
-        
-    execute: function(args, loop, cancellable) {
-	this._initSnapshot(args.prefix, args.snapshot, cancellable);
-	this.args = args;
+
+	this.prefix = prefix;
+	this.patchdir = this.workdir.get_child('patches');
+	let snapshotDir = this.workdir.get_child('snapshots');
+	let srcdb = new JsonDB.JsonDB(snapshotDir.get_child(prefix));
+	let snapshotPath = srcdb.getLatestPath();
+	let workingSnapshotPath = this.subworkdir.get_child(snapshotPath.get_basename());
+	GSystem.file_linkcopy(snapshotPath, workingSnapshotPath, Gio.FileCopyFlags.OVERWRITE,
+			      cancellable);
+	let data = srcdb.loadFromPath(workingSnapshotPath, cancellable);
+	this._snapshot = new Snapshot.Snapshot(data, workingSnapshotPath);
 
         let components = this._snapshot.data['components'];
 
-	let buildresultDir = this.workdir.get_child('builds').get_child(this.prefix);
-	let builddb = new JsonDB.JsonDB(buildresultDir);
+	let builddb = this._getResultDb('build/' + this.prefix);
 
-	let targetSourceVersion = builddb.parseVersionStr(this._snapshot.path.get_basename())
+	let targetSourceVersion = builddb.parseVersionStr(this._snapshot.path.get_basename());
 
 	let haveLocalComponent = false;
         for (let i = 0; i < components.length; i++) {
diff --git a/src/ostbuild/js/tasks/task-builddisks.js b/src/ostbuild/js/tasks/task-builddisks.js
new file mode 100644
index 0000000..c1bdf11
--- /dev/null
+++ b/src/ostbuild/js/tasks/task-builddisks.js
@@ -0,0 +1,141 @@
+// -*- indent-tabs-mode: nil; tab-width: 2; -*-
+// Copyright (C) 2013 Colin Walters <walters verbum org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const Format = imports.format;
+
+const GSystem = imports.gi.GSystem;
+
+const Builtin = imports.builtin;
+const ArgParse = imports.argparse;
+const Task = imports.task;
+const ProcUtil = imports.procutil;
+const BuildUtil = imports.buildutil;
+const LibQA = imports.libqa;
+const JsonDB = imports.jsondb;
+const Config = imports.config;
+const JsonUtil = imports.jsonutil;
+const GuestFish = imports.guestfish;
+
+const IMAGE_RETAIN_COUNT = 2;
+
+const TaskBuildDisks = new Lang.Class({
+    Name: 'TaskBuildDisks',
+    Extends: Task.TaskDef,
+
+    TaskPattern: [/builddisks\/(.*?)$/, 'prefix'],
+
+    TaskAfterPrefix: '/build/',
+
+    // Legacy
+    _VERSION_RE: /^(\d+)\.(\d+)$/,
+
+    execute: function(cancellable) {
+        let prefix = this.vars['prefix'];
+
+        let subworkdir = Gio.File.new_for_path('.');
+
+	      let baseImageDir = this.workdir.get_child('images').get_child(prefix);
+        GSystem.file_ensure_directory(baseImageDir, true, cancellable);
+	      let currentImageLink = baseImageDir.get_child('current');
+	      let previousImageLink = baseImageDir.get_child('previous');
+
+	      let builddb = this._getResultDb('build/' + prefix);
+
+        let latestPath = builddb.getLatestPath();
+        let buildVersion = builddb.parseVersionStr(latestPath.get_basename());
+        let buildData = builddb.loadFromPath(latestPath, cancellable);
+
+        let targetImageDir = baseImageDir.get_child(buildVersion);
+
+        if (targetImageDir.query_exists(null)) {
+            print("Already created " + targetImageDir.get_path());
+            return;
+        }
+
+        let workImageDir = subworkdir.get_child('images');
+        GSystem.file_ensure_directory(workImageDir, true, cancellable);
+
+	      let targets = buildData['targets'];
+
+        let osname = buildData['snapshot']['osname'];
+
+        for (let targetName in targets) {
+            let targetRevision = buildData['targets'][targetName];
+	          let squashedName = targetName.replace(/\//g, '_');
+	          let diskName = prefix + '-' + squashedName + '-disk.qcow2';
+            let diskPath = workImageDir.get_child(diskName);
+            let prevPath = currentImageLink.get_child(diskName);
+            GSystem.shutil_rm_rf(diskPath, cancellable);
+            if (prevPath.query_exists(null)) {
+                LibQA.copyDisk(prevPath, diskPath, cancellable);
+            } else {
+                LibQA.createDisk(diskPath, cancellable);
+            }
+	          ProcUtil.runSync(['ostbuild', 'qa-pull-deploy', diskPath.get_path(), 
+			                        this.repo.get_path(), osname, targetName, targetRevision],
+			                       cancellable, { logInitiation: true });
+	      }
+
+        GSystem.file_rename(workImageDir, targetImageDir, cancellable);
+
+        let currentInfo = null;
+        try {
+            currentInfo = currentImageLink.query_info('standard::symlink-target', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
+        } catch (e) {
+            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
+                throw e;
+        }
+        if (currentInfo != null) {
+            let newPreviousTmppath = baseImageDir.get_child('previous-new.tmp');
+            let currentLinkTarget = currentInfo.get_symlink_target();
+            GSystem.shutil_rm_rf(newPreviousTmppath, cancellable);
+            newPreviousTmppath.make_symbolic_link(currentLinkTarget, cancellable);
+            GSystem.file_rename(newPreviousTmppath, previousImageLink, cancellable);
+        }
+        BuildUtil.atomicSymlinkSwap(baseImageDir.get_child('current'), targetImageDir, cancellable);
+
+        this._cleanOldVersions(baseImageDir, IMAGE_RETAIN_COUNT, cancellable);
+    },
+
+    _loadVersionsFrom: function(dir, cancellable) {
+	      let e = dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
+	      let info;
+	      let results = [];
+	      while ((info = e.next_file(cancellable)) != null) {
+	          let name = info.get_name();
+	          let match = this._VERSION_RE.exec(name);
+	          if (!match)
+		            continue;
+	          results.push(name);
+	      }
+	      results.sort(BuildUtil.compareVersions);
+	      return results;
+    },
+
+    _cleanOldVersions: function(dir, retain, cancellable) {
+	      let versions = this._loadVersionsFrom(dir, cancellable);
+	      while (versions.length > retain) {
+	          let child = dir.get_child(versions.shift());
+	          GSystem.shutil_rm_rf(child, cancellable);
+	      }
+    },
+
+});
diff --git a/src/ostbuild/js/tasks/task-resolve.js b/src/ostbuild/js/tasks/task-resolve.js
new file mode 100644
index 0000000..1080b8a
--- /dev/null
+++ b/src/ostbuild/js/tasks/task-resolve.js
@@ -0,0 +1,84 @@
+// Copyright (C) 2011 Colin Walters <walters verbum org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const Format = imports.format;
+
+const GSystem = imports.gi.GSystem;
+
+const JsonDB = imports.jsondb;
+const Builtin = imports.builtin;
+const Task = imports.task;
+const ProcUtil = imports.procutil;
+const JsonUtil = imports.jsonutil;
+const Snapshot = imports.snapshot;
+const Config = imports.config;
+const BuildUtil = imports.buildutil;
+const Vcs = imports.vcs;
+const ArgParse = imports.argparse;
+
+const TaskResolve = new Lang.Class({
+    Name: "TaskResolve",
+    Extends: Task.TaskDef,
+
+    TaskPattern: [/resolve\/(.*?)$/, 'prefix'],
+
+    DefaultParameters: {fetchAll: false,
+			fetchComponents: [],
+		        timeoutSec: 10},
+
+    execute: function(cancellable) {
+        this.prefix = this.vars['prefix'];
+        let manifest = this.config.getGlobal('manifest');
+	let manifestPath = Gio.File.new_for_path(manifest);
+	let data = JsonUtil.loadJson(manifestPath, cancellable);
+        this._snapshot = new Snapshot.Snapshot(data, manifestPath, { prepareResolve: true });
+	
+        let gitMirrorArgs = ['ostbuild', 'git-mirror', '--timeout-sec=' + this.parameters.timeoutSec,
+			     '--manifest=' + manifest];
+        if (this.parameters.fetchAll || this.parameters.fetchComponents.length > 0) {
+            gitMirrorArgs.push('--fetch');
+            gitMirrorArgs.push('-k');
+	    gitMirrorArgs.push.apply(gitMirrorArgs, this.parameters.fetchComponents);
+	}
+	ProcUtil.runSync(gitMirrorArgs, cancellable, { logInitiation: true });
+	
+	let componentNames = this._snapshot.getAllComponentNames();
+	for (let i = 0; i < componentNames.length; i++) {
+	    let component = this._snapshot.getComponent(componentNames[i]);
+            let src = component['src'];
+            let [keytype, uri] = Vcs.parseSrcKey(src);
+            let branch = component['branch'];
+            let tag = component['tag'];
+            let branchOrTag = branch || tag;
+            let mirrordir = Vcs.ensureVcsMirror(this.mirrordir, keytype, uri, branchOrTag, cancellable);
+            let revision = Vcs.describeVersion(mirrordir, branchOrTag);
+            component['revision'] = revision;
+	}
+
+	let snapshotdir = this.workdir.get_child('snapshots');
+	this._src_db = new JsonDB.JsonDB(snapshotdir.get_child(this.prefix));
+        let [path, modified] = this._src_db.store(this._snapshot.data, cancellable);
+        if (modified) {
+            print("New source snapshot: " + path.get_path());
+        } else {
+            print("Source snapshot unchanged: " + path.get_path());
+	}
+    }
+});
diff --git a/src/ostbuild/js/builtins/qa_smoketest.js b/src/ostbuild/js/tasks/task-smoketest.js
similarity index 66%
rename from src/ostbuild/js/builtins/qa_smoketest.js
rename to src/ostbuild/js/tasks/task-smoketest.js
index 1708e3c..3722ad5 100644
--- a/src/ostbuild/js/builtins/qa_smoketest.js
+++ b/src/ostbuild/js/tasks/task-smoketest.js
@@ -26,31 +26,35 @@ const GSystem = imports.gi.GSystem;
 const Builtin = imports.builtin;
 const ArgParse = imports.argparse;
 const ProcUtil = imports.procutil;
+const Task = imports.task;
 const LibQA = imports.libqa;
+const JSUtil = imports.jsutil;
 
 const TIMEOUT_SECONDS = 2 * 60;
 
-const QaSmoketest = new Lang.Class({
-    Name: 'QaSmoketest',
-    Extends: Builtin.Builtin,
 
-    DESCRIPTION: "Test booting and logging in",
+const RequiredMessageIDs = ["39f53479d3a045ac8e11786248231fbf", // graphical.target 
+                            "f77379a8490b408bbe5f6940505a777b",  // systemd-journald
+                            "0ce153587afa4095832d233c17a88001" // gnome-session startup ok
+                           ];
+const FailedMessageIDs = ["fc2e22bc6ee647b6b90729ab34a250b1", // coredump
+                          "10dd2dc188b54a5e98970f56499d1f73" // gnome-session required component failed
+                         ];
 
-    RequiredMessageIDs: ["39f53479d3a045ac8e11786248231fbf", // graphical.target 
-                         "f77379a8490b408bbe5f6940505a777b",  // systemd-journald
-                         "0ce153587afa4095832d233c17a88001" // gnome-session startup ok
-                        ],
-    FailedMessageIDs: ["fc2e22bc6ee647b6b90729ab34a250b1", // coredump
-                       "10dd2dc188b54a5e98970f56499d1f73" // gnome-session required component failed
-                      ], 
+const SmoketestOne = new Lang.Class({
+    Name: 'SmoketestOne',
 
+    _fail: function(message) {
+        this._failed = true;
+        this._failedMessage = message;
+    },
+    
     _onQemuExited: function(proc, result) {
         let [success, status] = ProcUtil.asyncWaitCheckFinish(proc, result);
         this._qemu = null;
         this._loop.quit();
         if (!success) {
-            this._failed = true;
-            print("Qemu exited with status " + status);
+            this._fail("Qemu exited with status " + status);
         }
     },
 
@@ -59,7 +63,7 @@ const QaSmoketest = new Lang.Class({
         for (let msgid in this._pendingRequiredMessageIds) {
             print("Did not see MESSAGE_ID=" + msgid);
         }
-        this._failed = true;
+        this._fail("Timed out");
         this._loop.quit();
     },
 
@@ -72,8 +76,7 @@ const QaSmoketest = new Lang.Class({
             this._journalDataStream.read_line_async(GLib.PRIORITY_DEFAULT, this._cancellable,
                                                     Lang.bind(this, this._onJournalReadLine));
         } catch (e) {
-            print("Open failed: " + e);
-            this._failed = true;
+            this._fail("Journal open failed: " + e);
             this._loop.quit();
         }
     },
@@ -84,10 +87,12 @@ const QaSmoketest = new Lang.Class({
         try {
             [line, len] = stream.read_line_finish_utf8(result);
         } catch (e) {
-            this._failed = true;
+            this._fail(e.toString());
             this._loop.quit();
             throw e;
         }
+        if (this._done || this._failed)
+            return;
         if (line) {
             let data = JSON.parse(line);
             let messageId = data['MESSAGE_ID'];
@@ -99,10 +104,9 @@ const QaSmoketest = new Lang.Class({
                     this._countPendingRequiredMessageIds--;
                     matched = true;
                 } else {
-                    for (let i = 0; i < this.FailedMessageIDs.length; i++) {
-                        if (messageId == this.FailedMessageIDs[i]) {
-                            print("Found failure message ID " + messageId);
-                            this._failed = true;
+                    for (let i = 0; i < FailedMessageIDs.length; i++) {
+                        if (messageId == FailedMessageIDs[i]) {
+                            this._fail("Found failure message ID " + messageId);
                             this._loop.quit();
                             matched = true;
                             break;
@@ -116,12 +120,15 @@ const QaSmoketest = new Lang.Class({
                                                         Lang.bind(this, this._onJournalReadLine));
             } else {
                 print("Found all required message IDs, exiting");
+                this._done = true;
                 this._loop.quit();
             }
         }
     },
 
     _onJournalChanged: function(monitor, file, otherFile, eventType) {
+        if (this._done || this._failed)
+            return;
         if (!this._openedJournal) {
             this._openedJournal = true;
             file.read_async(GLib.PRIORITY_DEFAULT,
@@ -134,14 +141,10 @@ const QaSmoketest = new Lang.Class({
         }
     },
 
-    _init: function() {
-        this.parent();
-        this.parser.addArgument('--monitor', { action: 'storeTrue' });
-        this.parser.addArgument('diskpath');
-    },
-
-    execute: function(args, loop, cancellable) {
-        this._loop = loop;
+    execute: function(subworkdir, prefix, diskPath, cancellable) {
+        print("Smoke testing disk " + diskPath.get_path());
+        this._loop = GLib.MainLoop.new(null, true);
+        this._done = false;
         this._failed = false;
         this._journalStream = null;
         this._journalDataStream = null;
@@ -149,22 +152,19 @@ const QaSmoketest = new Lang.Class({
         this._readingJournal = false;
         this._pendingRequiredMessageIds = {};
         this._countPendingRequiredMessageIds = 0;
-        for (let i = 0; i < this.RequiredMessageIDs.length; i++) {
-            this._pendingRequiredMessageIds[this.RequiredMessageIDs[i]] = true;
+        for (let i = 0; i < RequiredMessageIDs.length; i++) {
+            this._pendingRequiredMessageIds[RequiredMessageIDs[i]] = true;
             this._countPendingRequiredMessageIds += 1;
         }
         this._cancellable = cancellable;
 
-        let srcDiskpath = Gio.File.new_for_path(args.diskpath);
-        let workdir = Gio.File.new_for_path('.');
-        
         let qemuArgs = [LibQA.getQemuPath()];
         qemuArgs.push.apply(qemuArgs, LibQA.DEFAULT_QEMU_OPTS);
 
-        let diskClone = workdir.get_child('qa-smoketest.qcow2');
+        let diskClone = subworkdir.get_child('smoketest-' + diskPath.get_basename());
         GSystem.shutil_rm_rf(diskClone, cancellable);
 
-        LibQA.createDiskSnapshot(srcDiskpath, diskClone, cancellable);
+        LibQA.createDiskSnapshot(diskPath, diskClone, cancellable);
         let [gfmnt, mntdir] = LibQA.newReadWriteMount(diskClone, cancellable);
         try {
             LibQA.modifyBootloaderAppendKernelArgs(mntdir, ["console=ttyS0"], cancellable);
@@ -178,25 +178,17 @@ const QaSmoketest = new Lang.Class({
             gfmnt.umount(cancellable);
         }
 
-        let consoleOutput = Gio.File.new_for_path('console.out');
-        GSystem.shutil_rm_rf(consoleOutput, cancellable);
-        let journalOutput = Gio.File.new_for_path('journal-json.txt');
-        GSystem.shutil_rm_rf(journalOutput, cancellable);
+        let consoleOutput = subworkdir.get_child('console.out');
+        let journalOutput = subworkdir.get_child('journal-json.txt');
 
         qemuArgs.push.apply(qemuArgs, ['-drive', 'file=' + diskClone.get_path() + ',if=virtio',
                                        '-vnc', 'none',
-                                       '-watchdog', 'ib700',
-                                       '-watchdog-action', 'poweroff',
                                        '-serial', 'file:' + consoleOutput.get_path(),
                                        '-device', 'virtio-serial',
                                        '-chardev', 'file,id=journaljson,path=' + journalOutput.get_path(),
                                        '-device', 'virtserialport,chardev=journaljson,name=org.gnome.journaljson']);
-        if (args.monitor)
-            qemuArgs.push.apply(qemuArgs, ['-monitor', 'stdio']);
         
         let qemuContext = new GSystem.SubprocessContext({ argv: qemuArgs });
-        if (args.monitor)
-            qemuContext.set_stdin_disposition(GSystem.SubprocessStreamDisposition.INHERIT);
         let qemu = new GSystem.Subprocess({context: qemuContext});
         this._qemu = qemu;
         print("starting qemu");
@@ -207,19 +199,47 @@ const QaSmoketest = new Lang.Class({
         let journalMonitor = journalOutput.monitor_file(0, cancellable);
         journalMonitor.connect('changed', Lang.bind(this, this._onJournalChanged));
 
-        GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, TIMEOUT_SECONDS,
-                                 Lang.bind(this, this._onTimeout));
+        let timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, TIMEOUT_SECONDS,
+                                                 Lang.bind(this, this._onTimeout));
         
-        loop.run();
+        this._loop.run();
 
         if (this._qemu)
             this._qemu.force_exit();
+
+        GLib.source_remove(timeoutId);
         
         if (this._failed) {
-            print("Exiting abnormally");
-            return 1;
+            throw new Error(this._failedMessage);
+        }
+        print("Completed smoke testing of " + diskPath.get_basename());
+    }
+});
+
+const TaskSmoketest = new Lang.Class({
+    Name: 'TaskSmoketest',
+    Extends: Task.TaskDef,
+
+    TaskPattern: [/smoketest\/(.*?)$/, 'prefix'],
+
+    execute: function(cancellable) {
+        let prefix = this.vars['prefix'];
+
+	      let imageDir = this.workdir.get_child('images').get_child(prefix);
+	      let currentImages = imageDir.get_child('current');
+
+        let e = currentImages.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
+                                                 cancellable);
+        let info;
+        while ((info = e.next_file(cancellable)) != null) {
+            let name = info.get_name();
+            if (!JSUtil.stringEndswith(name, '.qcow2'))
+                continue;
+            let workdirName = 'work-' + name.replace(/\.qcow2$/, '');
+            let subworkdir = Gio.File.new_for_path(workdirName);
+            GSystem.file_ensure_directory(subworkdir, true, cancellable);
+            let smokeTest = new SmoketestOne();
+            smokeTest.execute(subworkdir, prefix, currentImages.get_child(name), cancellable);
         }
-        print("Complete!");
-        return 0;
     }
 });
diff --git a/src/ostbuild/js/vcs.js b/src/ostbuild/js/vcs.js
index e2f82ad..3d324c2 100644
--- a/src/ostbuild/js/vcs.js
+++ b/src/ostbuild/js/vcs.js
@@ -173,22 +173,40 @@ function _listSubmodules(mirrordir, mirror, keytype, uri, branch, cancellable) {
 
 function ensureVcsMirror(mirrordir, keytype, uri, branch, cancellable,
 			 params) {
-    params = Params.parse(params, {fetch: false,
-				   fetchKeepGoing: false});
+    params = Params.parse(params, { fetch: false,
+				    fetchKeepGoing: false,
+				    timeoutSec: 0 });
+    let fetch = params.fetch;
     let mirror = getMirrordir(mirrordir, keytype, uri);
     let tmpMirror = mirror.get_parent().get_child(mirror.get_basename() + '.tmp');
     let didUpdate = false;
     let lastFetchPath = getLastfetchPath(mirrordir, keytype, uri, branch);
     let lastFetchContents = null;
-    if (lastFetchPath.query_exists(cancellable)) {
+    let currentTime = GLib.DateTime.new_now_utc();
+    let lastFetchContents = null;
+    let lastFetchInfo = null;
+    try {
+	lastFetchInfo = lastFetchPath.query_info('time::modified', Gio.FileQueryInfoFlags.NONE, cancellable);
+    } catch (e) {
+	if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
+	    throw e;
+    }
+    if (lastFetchInfo != null) {
 	lastFetchContents = GSystem.file_load_contents_utf8(lastFetchPath, cancellable).replace(/[ \n]/g, '');
+	if (params.timeoutSec > 0) {
+	    let lastFetchTime = GLib.DateTime.new_from_unix_local(lastFetchInfo.get_attribute_uint64('time::modified'));
+	    let diff = currentTime.difference(lastFetchTime) / 1000 / 1000;
+	    if (diff < params.timeoutSec) {
+		fetch = false;
+	    }
+	}
     }
     GSystem.shutil_rm_rf(tmpMirror, cancellable);
     if (!mirror.query_exists(cancellable)) {
         ProcUtil.runSync(['git', 'clone', '--mirror', uri, tmpMirror.get_path()], cancellable);
         ProcUtil.runSync(['git', 'config', 'gc.auto', '0'], cancellable, {cwd: tmpMirror});
         GSystem.file_rename(tmpMirror, mirror, cancellable);
-    } else if (params.fetch) {
+    } else if (fetch) {
 	try {
             ProcUtil.runSync(['git', 'fetch'], cancellable, {cwd:mirror});
 	} catch (e) {
@@ -210,17 +228,24 @@ function ensureVcsMirror(mirrordir, keytype, uri, branch, cancellable,
 	});
     }
     
-    if (changed) {
+    if (changed || (fetch && params.timeoutSec > 0)) {
 	lastFetchPath.replace_contents(currentVcsVersion, null, false, 0, cancellable); 
     }
 
     return mirror;
 }
 
+function uncacheRepository(mirrordir, keytype, uri, branch, cancellable) {
+    let lastFetchPath = getLastfetchPath(mirrordir, keytype, uri, branch);
+    GSystem.shutil_rm_rf(lastFetchPath, cancellable);
+}
+
 function fetch(mirrordir, keytype, uri, branch, cancellable, params) {
-    params = Params.parse(params, {keepGoing: false});
+    params = Params.parse(params, {keepGoing: false, timeoutSec: 0});
     ensureVcsMirror(mirrordir, keytype, uri, branch, cancellable,
-		      {fetch:true, fetchKeepGoing: params.keepGoing});
+		      { fetch:true,
+			fetchKeepGoing: params.keepGoing,
+			timeoutSec: params.timeoutSec });
 }
 
 function describeVersion(dirpath, branch) {
diff --git a/src/ostbuild/ostbuild.in b/src/ostbuild/ostbuild.in
index 01d75bc..258d9d0 100755
--- a/src/ostbuild/ostbuild.in
+++ b/src/ostbuild/ostbuild.in
@@ -25,4 +25,4 @@ export GIO_USE_VFS=local
 export OSTBUILD_DATADIR= pkgdatadir@
 export OSTBUILD_LIBDIR= pkglibdir@
 
-exec gjs -I "${jsdir}" "${jsdir}/main.js" "$@"
+exec $OSTBUILD_GDB gjs -I "${jsdir}" "${jsdir}/main.js" "$@"


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]