[gnome-ostree/wip/gjs: 2/2] Depend on gjs, rewrite autobuilder.py using it



commit e9a84afb1e381822c2df9e5aabe3917bfbd36211
Author: Colin Walters <walters verbum org>
Date:   Mon Nov 5 08:17:13 2012 -0500

    Depend on gjs, rewrite autobuilder.py using it
    
    This way we get to use the GNOME platform, which allows us to feed
    back improvements.  Specifically I found myself too limited by
    Python's lack of a mainloop.  We can now run multiple processes at
    once easily.

 .gitmodules                                    |    3 +
 Makefile-ostbuild.am                           |   34 +++-
 Makefile.am                                    |   28 +++
 autogen.sh                                     |    8 +
 configure.ac                                   |   24 ++
 src/libgsystem                                 |    1 +
 src/ostbuild/js/autobuilder.js                 |  290 ++++++++++++++++++++++++
 src/ostbuild/js/config.js                      |   42 ++++
 src/ostbuild/js/jsondb.js                      |   85 +++++++
 src/ostbuild/js/jsonutil.js                    |   19 ++
 src/ostbuild/js/procutil.js                    |   30 +++
 src/ostbuild/js/snapshot.js                    |   54 +++++
 src/ostbuild/js/task.js                        |  130 +++++++++++
 src/ostbuild/ostbuild-js.in                    |   12 +
 src/ostbuild/pyostbuild/builtin_autobuilder.py |  263 ---------------------
 src/ostbuild/pyostbuild/main.py                |   18 ++-
 16 files changed, 767 insertions(+), 274 deletions(-)
---
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..f456a14
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "src/libgsystem"]
+	path = src/libgsystem
+	url = git://git.gnome.org/libgsystem
diff --git a/Makefile-ostbuild.am b/Makefile-ostbuild.am
index d93a760..45c8231 100644
--- a/Makefile-ostbuild.am
+++ b/Makefile-ostbuild.am
@@ -15,16 +15,25 @@
 # Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 # Boston, MA 02111-1307, USA.
 
+substitutions= \
+	-e s,@libdir\@,$(libdir), \
+	-e s,@pkglibdir\@,$(pkglibdir), \
+	-e s,@datarootdir\@,$(datarootdir), \
+	-e s,@pkgdatadir\@,$(pkgdatadir), \
+	-e s,@PYTHON\@,$(PYTHON), \
+	$(NULL)
+
+
 ostbuild: src/ostbuild/ostbuild.in Makefile
-	sed -e s,@libdir\@,$(libdir), \
-	    -e s,@datarootdir\@,$(datarootdir), \
-	    -e s,@pkgdatadir\@,$(pkgdatadir), \
-	    -e s,@PYTHON\@,$(PYTHON), \
-	    $< > $  tmp && mv $  tmp $@
+	sed $(substitutions) $< > $  tmp && mv $  tmp $@
 EXTRA_DIST += ostbuild/ostbuild.in
 
+ostbuild-js: src/ostbuild/ostbuild-js.in Makefile
+	sed $(substitutions) $< > $  tmp && mv $  tmp $@
+EXTRA_DIST += ostbuild/ostbuild-js.in
+
 if BUILDSYSTEM
-bin_SCRIPTS += ostbuild 
+bin_SCRIPTS += ostbuild ostbuild-js
 
 utils_SCRIPTS = \
 	src/ostbuild/ostree-build-compile-one \
@@ -35,7 +44,6 @@ utilsdir = $(libdir)/ostbuild
 pyostbuilddir=$(libdir)/ostbuild/pyostbuild
 pyostbuild_PYTHON =					\
 	src/ostbuild/pyostbuild/buildutil.py		\
-	src/ostbuild/pyostbuild/builtin_autobuilder.py	\
 	src/ostbuild/pyostbuild/builtin_build.py	\
 	src/ostbuild/pyostbuild/builtin_checkout.py	\
 	src/ostbuild/pyostbuild/builtin_deploy_qemu.py	\
@@ -67,4 +75,16 @@ pyostbuild_PYTHON =					\
 	src/ostbuild/pyostbuild/subprocess_helpers.py	\
 	src/ostbuild/pyostbuild/vcs.py			\
 	$(NULL)
+
+jsostbuilddir=$(pkgdatadir)/js
+jsostbuild_DATA= \
+	src/ostbuild/js/autobuilder.js \
+	src/ostbuild/js/config.js \
+	src/ostbuild/js/jsondb.js \
+	src/ostbuild/js/task.js \
+	src/ostbuild/js/procutil.js \
+	src/ostbuild/js/snapshot.js \
+	src/ostbuild/js/jsonutil.js \
+	$(NULL)
+
 endif
diff --git a/Makefile.am b/Makefile.am
index 963bff0..f9b7fb7 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -34,8 +34,36 @@ privlibdir = $(pkglibdir)
 privlib_LTLIBRARIES =
 INSTALL_DATA_HOOKS =
 
+libgsystem_srcpath := src/libgsystem
+libgsystem_cflags = $(GIO_UNIX_CFLAGS) -I$(srcdir)/src/libgsystem
+libgsystem_libs = $(GIO_UNIX_LIBS)
+include src/libgsystem/Makefile-libgsystem.am
+privlib_LTLIBRARIES += libgsystem.la
 include Makefile-ostbuild.am
 
+include $(INTROSPECTION_MAKEFILE)
+INTROSPECTION_GIRS = GSystem-1.0.gir
+
+GSystem-1.0.gir: libgsystem.la Makefile
+GSystem_1_0_gir_NAMESPACE = GSystem
+GSystem_1_0_gir_VERSION = 1.0
+GSystem_1_0_gir_LIBS = libgsystem.la
+GSystem_1_0_gir_CFLAGS = $(libgsystem_cflags)
+GSystem_1_0_gir_SCANNERFLAGS =			\
+       --warn-all				\
+       --symbol-prefix=gs_			\
+       --identifier-prefix=GS			\
+        --c-include="libgsystem.h"	        \
+       $(NULL)
+GSystem_1_0_gir_INCLUDES = Gio-2.0
+GSystem_1_0_gir_FILES = $(libgsystem_la_SOURCES)
+
+girdir= $(pkgdatadir)/gir-1.0
+typelibdir= $(pkglibdir)/girepository-1.0
+
+gir_DATA = $(INTROSPECTION_GIRS)
+typelib_DATA = $(gir_DATA:.gir=.typelib)
+
 install-data-hook: $(INSTALL_DATA_HOOKS)
 
 release-tag:
diff --git a/autogen.sh b/autogen.sh
index 83779c7..348b6c9 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -16,6 +16,14 @@ fi
 
 mkdir -p m4
 
+# Fetch submodules if needed
+if test ! -f src/libgsystem/README;
+then
+  echo "+ Setting up submodules"
+  git submodule init
+  git submodule update
+fi
+
 autoreconf --force --install --verbose
 
 cd $olddir
diff --git a/configure.ac b/configure.ac
index 67db4a7..febaec5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -18,6 +18,27 @@ AM_INIT_AUTOMAKE([1.11 -Wno-portability foreign no-define tar-ustar no-dist-gzip
 AM_MAINTAINER_MODE([enable])
 AM_SILENT_RULES([yes])
 
+AC_PROG_CC
+AM_PROG_CC_C_O
+
+changequote(,)dnl
+if test "x$GCC" = "xyes"; then
+  WARN_CFLAGS="-Wall -Werror=strict-prototypes -Werror=missing-prototypes \
+		-Werror=implicit-function-declaration \
+		-Werror=pointer-arith -Werror=init-self -Werror=format=2 \
+		-Werror=format-security \
+		-Werror=missing-include-dirs -Werror=aggregate-return \
+		-Werror=declaration-after-statement"
+fi
+changequote([,])dnl
+AC_SUBST(WARN_CFLAGS)
+
+# Initialize libtool
+LT_PREREQ([2.2.4])
+LT_INIT([disable-static])
+
+GOBJECT_INTROSPECTION_REQUIRE([1.34.0])
+
 AM_PATH_PYTHON([2.6])
 
 AC_ARG_ENABLE(buildsystem,
@@ -25,6 +46,9 @@ AC_ARG_ENABLE(buildsystem,
 	    enable_buildsystem=yes)
 AM_CONDITIONAL(BUILDSYSTEM, test x$enable_buildsystem != xno)
 
+PKG_CHECK_MODULES(GIO_UNIX, [gio-unix-2.0 >= 2.34.0])
+GIO_UNIX_CFLAGS="$GIO_UNIX_CFLAGS -DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_34 -DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_34"
+
 AC_CONFIG_FILES([
 Makefile
 ])
diff --git a/src/libgsystem b/src/libgsystem
new file mode 160000
index 0000000..c17376d
--- /dev/null
+++ b/src/libgsystem
@@ -0,0 +1 @@
+Subproject commit c17376d4acbddfa1909d24167d3ce24531b3db1a
diff --git a/src/ostbuild/js/autobuilder.js b/src/ostbuild/js/autobuilder.js
new file mode 100644
index 0000000..735bf80
--- /dev/null
+++ b/src/ostbuild/js/autobuilder.js
@@ -0,0 +1,290 @@
+#!/usr/bin/env gjs
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const Format = imports.format;
+
+const GSystem = imports.gi.GSystem;
+
+const Task = imports.task;
+const JsonDB = imports.jsondb;
+const ProcUtil = imports.procutil;
+const JsonUtil = imports.jsonutil;
+const Snapshot = imports.snapshot;
+
+const loop = GLib.MainLoop.new(null, true);
+
+var AutoBuilderIface = <interface name="org.gnome.OSTreeBuild.AutoBuilder">
+<method name="queueBuild">
+    <arg type="as" direction="in" />
+</method>
+<method name="queueResolve">
+    <arg type="as" direction="in" />
+</method>
+<property name="Status" type="s" access="read" />
+</interface>;
+
+const AutoBuilder = new Lang.Class({
+    Name: 'AutoBuilder',
+    
+    _init: function() {
+	this._resolve_proc = null;
+	this._build_proc = null;
+
+	this.config = imports.config.get();
+	this.workdir = Gio.File.new_for_path(this.config.getGlobal('workdir'));
+	this.prefix = this.config.getPrefix();
+	this._snapshot_dir = this.workdir.get_child('snapshots');
+	this._status_path = this.workdir.get_child('autobuilder-' + this.prefix + '.json');
+
+	this._build_needed = true;
+	this._full_resolve_needed = true;
+	this._queued_force_builds = [];
+	this._queued_force_resolve = [];
+	this._autoupdate_self = false;
+	this._resolve_timeout = 0;
+	this._resolve_is_full = false;
+	this._source_snapshot_path = null;
+	this._prev_source_snapshot_path = null;
+	
+	this._impl = Gio.DBusExportedObject.wrapJSObject(AutoBuilderIface, this);
+	this._impl.export(Gio.DBus.session, '/org/gnome/OSTreeBuild/AutoBuilder');
+
+	let snapshotdir = this.workdir.get_child('snapshots');
+	GSystem.file_ensure_directory(snapshotdir, true, null);
+	this._src_db = new JsonDB.JsonDB(snapshotdir, this.prefix + '-src-snapshot');
+
+	let taskdir = new Task.TaskDir(this.workdir.get_child('tasks'));
+	this._resolve_taskset = taskdir.get(this.prefix + '-resolve');
+	this._build_taskset = taskdir.get(this.prefix + '-build');
+
+	this._source_snapshot_path = this._src_db.getLatestPath();
+
+	this._status_path = this.workdir.get_child('autobuilder-' + this.prefix + '.json');
+
+	this._fetch();
+	if (this._source_snapshot_path != null)
+	    this._run_build();
+
+	this._updateStatus();
+    },
+
+    _updateStatus: function() {
+	let newStatus = "";
+	if (this._resolve_proc == null && this._build_proc == null) {
+	    newStatus = "idle";
+	} else {
+	    if (this._resolve_proc != null)
+		newStatus += "[resolving] ";
+	    if (this._build_proc != null)
+		newStatus += "[building] ";
+	}
+	if (newStatus != this._status) {
+	    this._status = newStatus;
+	    print(this._status);
+	    this._impl.emit_property_changed('Status', new GLib.Variant("s", this._status));
+	}
+
+	this._writeStatusFile();
+    },
+
+    get Status() {
+	return this._status;
+    },
+
+    queueBuild: function(components) {
+	this._queued_force_builds.push.apply(this._queued_force_builds, components);
+	this._build_needed = true;
+	log("queued builds: " + this._queued_force_builds);
+	if (this._build_proc == null)
+	    this._run_build();
+    },
+
+    queueResolve: function(components) {
+	this._queued_force_resolve.push.apply(this._queued_force_resolve, components);
+	log("queued resolves: " + this._queued_force_resolve);
+	if (this._resolve_proc == null)
+	    this._fetch();
+    },
+
+    _fetch: function() {
+	if (this._resolve_proc != null) {
+	    this._full_resolve_needed = true;
+	    return false;
+	}
+	let t = this._resolve_taskset.start();
+	let taskWorkdir = t.path;
+
+	if (this._autoupdate_self)
+	    ProcUtil.runSync(['git', 'pull', '-r'])
+
+	let args = ['ostbuild', 'resolve', '--manifest=manifest.json',
+		    '--fetch', '--fetch-keep-going'];
+	if (this._queued_force_resolve.length > 0) {
+	    this._resolve_is_full = false;
+	    args.push.apply(args, this._queued_force_resolve);
+	} else {
+	    this._resolve_is_full = true;
+	}
+	this._queued_force_resolve = [];
+	let context = new GSystem.SubprocessContext({ argv: args });
+	context.set_stdout_file_path(t.logfile_path.get_path());
+	context.set_stderr_disposition(GSystem.SubprocessStreamDisposition.STDERR_MERGE);
+	this._resolve_proc = new GSystem.Subprocess({context: context});
+	this._resolve_proc.init(null);
+	log(Format.vprintf("Resolve task %s.%s started, pid=%s", [t.major, t.minor, this._resolve_proc.get_pid()]));
+	this._resolve_proc.wait(null, Lang.bind(this, this._onResolveExited));
+
+	this._updateStatus();
+
+	return false;
+    },
+
+    _onResolveExited: function(process, result) {
+	this._resolve_proc = null;
+	let [success, msg] = ProcUtil.asyncWaitCheckFinish(process, result);
+	log(Format.vprintf("resolve exited; success=%s msg=%s", [success, msg]))
+	this._resolve_taskset.finish(success);
+	this._prev_source_snapshot_path = this._source_snapshot_path;
+	this._source_snapshot_path = this._src_db.getLatestPath();
+	if (this._resolve_is_full) {
+	    this._resolve_timeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10*60,
+							     Lang.bind(this, this._fetch));
+	}
+	let changed = (this._prev_source_snapshot_path == null ||
+		       !this._prev_source_snapshot_path.equal(this._source_snapshot_path));
+        if (changed)
+            log(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_proc == null)
+	    this._run_build();
+
+	if (this._full_resolve_needed) {
+	    this._full_resolve_needed = false;
+	    this._fetch();
+	}
+
+	this._updateStatus();
+    },
+    
+    _run_build: function() {
+	let cancellable = null;
+	if (this._build_proc != null) throw new Error();
+	if (!this._build_needed) throw new Error();
+
+	this._build_needed = false;
+
+	let task = this._build_taskset.start();
+	let workdir = task.path;
+	let args = ['ostbuild', 'build', '--skip-vcs-matches',
+		    '--src-snapshot=' + this._source_snapshot_path.get_path()];
+	args.push.apply(args, this._queued_force_builds);
+	this._queued_force_builds = [];
+
+	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 context = new GSystem.SubprocessContext({ argv: args });
+	context.set_stdout_file_path(task.logfile_path.get_path());
+	context.set_stderr_disposition(GSystem.SubprocessStreamDisposition.STDERR_MERGE);
+	this._build_proc = new GSystem.Subprocess({context: context});
+	this._build_proc.init(null);
+	log(Format.vprintf("Build task %s.%s started, pid=%s", [task.major, task.minor, this._build_proc.get_pid()]));
+	this._build_proc.wait(null, Lang.bind(this, this._onBuildExited));
+
+	this._updateStatus();
+    },
+
+    _onBuildExited: function(process, result) {
+	if (this._build_proc == null) throw new Error();
+	this._build_proc = null;
+	let [success, msg] = ProcUtil.asyncWaitCheckFinish(process, result);
+	log(Format.vprintf("build exited; success=%s msg=%s", [success, msg]))
+	this._build_taskset.finish(success);
+	if (this._build_needed)
+	    this._run_build()
+	
+	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: Format.vprintf('%d.%d', [item.major, item.minor]),
+			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);
+    }
+});
+
+var ownId = Gio.DBus.session.own_name('org.gnome.OSTreeBuild', Gio.BusNameOwnerFlags.NONE,
+				      function(name) {},
+				      function(name) { loop.quit(); });
+
+var builder = new AutoBuilder();
+loop.run();
diff --git a/src/ostbuild/js/config.js b/src/ostbuild/js/config.js
new file mode 100644
index 0000000..d006387
--- /dev/null
+++ b/src/ostbuild/js/config.js
@@ -0,0 +1,42 @@
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+
+const GSystem = imports.gi.GSystem;
+
+const Config = new Lang.Class({
+    Name: 'Config',
+
+    _init: function() {
+	this._keyfile = new GLib.KeyFile();
+	var path = GLib.build_filenamev([GLib.get_user_config_dir(), "ostbuild.cfg"]);
+	this._keyfile.load_from_file(path, GLib.KeyFileFlags.NONE);
+    },
+
+    getGlobal: function(key, defaultValue)  {
+	try {
+	    return this._keyfile.get_string("global", key);
+	} catch (e) {
+	    if (e.domain == GLib.KeyFileError
+		&& defaultValue != undefined)
+		return defaultValue;
+	    throw e;
+	}
+    },
+
+    getPrefix: function() {
+	let pathname = GLib.build_filenamev([GLib.get_user_config_dir(), "ostbuild-prefix"]);
+	let path = Gio.File.new_for_path(pathname);
+	if (!path.query_exists(null))
+	    throw new Error("No prefix set; use \"ostbuild prefix\" to set one");
+	let prefix = GSystem.file_load_contents_utf8(path, null);
+	return prefix.replace(/[ \r\n]/g, '');
+    }
+});
+
+var _instance = null;
+
+function get() {
+    if (_instance == null)
+	_instance = new Config();
+    return _instance;
+}
diff --git a/src/ostbuild/js/jsondb.js b/src/ostbuild/js/jsondb.js
new file mode 100644
index 0000000..7628c5a
--- /dev/null
+++ b/src/ostbuild/js/jsondb.js
@@ -0,0 +1,85 @@
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const Format = imports.format;
+
+const JsonUtil = imports.jsonutil;
+
+const JsonDB = new Lang.Class({
+    Name: 'JsonDB',
+
+    _init: function(path, prefix) {
+	this._path = path;
+	this._prefix = prefix;
+	this._re = /-(\d+)\.(\d+)-([0-9a-f]+).json$/;
+    },
+
+    parseVersion: function(basename) {
+	let match = this._re.exec(basename);
+	if (!match)
+	    throw new Error("No JSONDB version in " + basename);
+	return [parseInt(match[1]), parseInt(match[2])];
+    },
+
+    parseVersionStr: function(basename) {
+	let [major, minor] = this.parseVersion(basename);
+	return Format.vprintf('%d.%d', [major, minor]);
+    },
+
+    _getAll: function() {
+	var result = [];
+	var e = this._path.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
+	let info;
+	while ((info = e.next_file(null)) != null) {
+	    let name = info.get_name();
+	    if (name.indexOf(this._prefix) != 0)
+		continue;
+	    if (name.lastIndexOf('.json') != name.length-5)
+		continue;
+	    let match = this._re.exec(name);
+	    if (!match)
+		throw new Error("Invalid JSONDB file " + name);
+	    result.push([parseInt(match[1]), parseInt(match[2]),
+			 match[3], name]);
+	}
+	result.sort(function(a, b) {
+	    var aMajor = a[0]; var bMajor = b[0];
+	    var aMinor = a[1]; var bMinor = b[1];
+	    if (aMajor < bMajor) return 1;
+	    else if (aMajor > bMajor) return -1;
+	    else if (aMinor < bMinor) return 1;
+	    else if (aMinor > bMinor) return -1;
+	    else return 0;
+	});
+	return result;
+    },
+
+    getLatestPath: function() {
+	let all = this._getAll();
+	if (all.length == 0)
+	    return null;
+	return this._path.get_child(all[0][3]);
+    },
+
+    getPreviousPath: function(path) {
+        let name = path.get_basename();
+	let [target_major, target_minor] = this.parseVersion(name);
+	let files = this._getAll();
+        let prev = null;
+        let found = false;
+	for (let i = files.length - 1; i >= 0; i--) {
+	    let [major, minor, csum, fname] = files[i];
+            if (target_major == major && target_minor == minor) {
+                found = true;
+                break;
+	    }
+            prev = fname;
+	}
+        if (found)
+            return this._path.get_child(prev);
+        return null;
+    },
+
+    loadFromPath: function(path, cancellable) {
+	return JsonUtil.loadJson(this._path.get_child(path.get_basename()), cancellable);
+    }
+});
diff --git a/src/ostbuild/js/jsonutil.js b/src/ostbuild/js/jsonutil.js
new file mode 100644
index 0000000..ab64073
--- /dev/null
+++ b/src/ostbuild/js/jsonutil.js
@@ -0,0 +1,19 @@
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+
+/* jsonutil.js:
+ * Read/write JSON to/from GFile paths, very inefficiently.
+ */
+
+function writeJsonFileAtomic(path, data, cancellable) {
+    let buf = JSON.stringify(data, null, "  ");
+    let s = path.replace(null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
+    s.write_bytes(new GLib.Bytes(buf), cancellable);
+    s.close(cancellable);
+}
+
+function loadJson(path, cancellable) {
+    let [success,contents,etag] = path.load_contents(cancellable);
+    return JSON.parse(contents);
+}
+
diff --git a/src/ostbuild/js/procutil.js b/src/ostbuild/js/procutil.js
new file mode 100644
index 0000000..69c65b6
--- /dev/null
+++ b/src/ostbuild/js/procutil.js
@@ -0,0 +1,30 @@
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+
+const GSystem = imports.gi.GSystem;
+
+function runSync(args, stdoutDisposition, stderrDisposition) {
+    if (stdoutDisposition == undefined)
+	stdoutDisposition = GSystem.SubprocessStreamDisposition.INHERIT;
+    if (stderrDisposition == undefined)
+	stderrDisposition = GSystem.SubprocessStreamDisposition.INHERIT;
+    var proc = GSystem.Subprocess.new_simple_argv(args, stdoutDisposition, stderrDisposition);
+    proc.wait_sync_check(null);
+}
+
+
+function asyncWaitCheckFinish(process, result) {
+    let [waitSuccess, estatus] = process.wait_finish(result);
+    let success = false;
+    let errorMsg = null;
+    try {
+	GLib.spawn_check_exit_status(estatus);
+	return [true, null];
+    } catch (e) {
+	if (e.domain == GLib.spawn_exit_error_quark() ||
+	    e.matches(GLib.SpawnError, GLib.SpawnError.FAILED))
+	    return [false, e.message];
+	else
+	    throw e;
+    }
+}
diff --git a/src/ostbuild/js/snapshot.js b/src/ostbuild/js/snapshot.js
new file mode 100644
index 0000000..dc2216f
--- /dev/null
+++ b/src/ostbuild/js/snapshot.js
@@ -0,0 +1,54 @@
+//
+// Copyright (C) 2012 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 _componentDict(snapshot) {
+    let r = {};
+    for (let component in snapshot['components']) {
+        r[component['name']] = component;
+    }
+    let patches = snapshot['patches'];
+    r[patches['name']] = patches;
+    let base = snapshot['base'];
+    r[base['name']] = base;
+    return r;
+}
+
+function snapshotDiff(a, b) {
+    let a_components = _componentDict(a);
+    let b_components = _componentDict(b);
+
+    let added = [];
+    let modified = [];
+    let removed = [];
+
+    for (let name in a_components) {
+        let c_a = a_components[name]
+        let c_b = b_components[name]
+        if (c_b == undefined) {
+            removed.push(name);
+	} else if (c_a['revision'] != c_b['revision']) {
+            modified.push(name);
+	}
+    }
+    for (let name in b_components) {
+        if (a_components[name] == undefined) {
+            added.push(name);
+	}
+    }
+    return [added, modified, removed];
+}
diff --git a/src/ostbuild/js/task.js b/src/ostbuild/js/task.js
new file mode 100644
index 0000000..018aaf8
--- /dev/null
+++ b/src/ostbuild/js/task.js
@@ -0,0 +1,130 @@
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const format = imports.format;
+
+const GSystem = imports.gi.GSystem;
+
+const VERSION_RE = /(\d+)\.(\d+)/;
+
+const TaskDir = new Lang.Class({
+    Name: 'TaskDir',
+
+    _init: function(path) {
+	this.path = path;
+    },
+
+    get: function(name) {
+	let child = this.path.get_child(name);
+	GSystem.file_ensure_directory(child, true, null);
+
+	return new TaskSet(child);
+    }
+});
+
+const TaskHistoryEntry = new Lang.Class({
+    Name: 'TaskHistoryEntry',
+
+    _init: function(path, state) {
+	this.path = path;
+	let match = VERSION_RE.exec(path.get_basename());
+	this.major = parseInt(match[1]);
+	this.minor = parseInt(match[2]);
+	this.timestamp = null;
+	this.logfile_path = null;
+	this.start_timestamp = null;
+
+	if (state == undefined) {
+	    let statusPath = this.path.get_child('status');
+	    if (statusPath.query_exists(null)) {
+		let ioStream = statusPath.read(null);
+		let info = ioStream.query_info("unix::mtime", null);
+		let contents = ioStream.read_bytes(8192, null);
+		this.state = contents;
+		ioStream.close(null);
+		this.timestamp = info.get_attribute_uint64("time::modified");
+	    } else {
+		this.state = 'interrupted';
+	    }
+	} else {
+	    this.state = state;
+	    this.start_timestamp = new Date().getTime() / 1000;
+	}
+    },
+
+    finish: function(success) {
+	let statusPath = this.path.get_child('status');
+	this.state = success ? 'success' : 'failed';
+	statusPath.replace_contents(this.state, null, false, 0, null);
+	this.timestamp = new Date().getTime() / 1000;
+    },
+
+    compareTo: function(a, b) {
+	function cmp(a, b) {
+	    if (a == b) return 0;
+	    else if (a < b) return -1;
+	    else return 1;
+	}
+	let c = cmp(a.major, b.major);
+	if (c != 0) return c;
+	return cmp(a.minor, b.minor);
+    }
+});
+
+const TaskSet = new Lang.Class({
+    Name: 'TaskSet',
+
+    _init: function(path, prefix) {
+	this.path = path;
+
+	this._history = [];
+	this._running = false;
+	this._running_version = null;
+	
+	this._load();
+    },
+
+    _load: function() {
+	var e = this.path.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
+	let info;
+	while ((info = e.next_file(null)) != null) {
+	    let name = info.get_name();
+	    let childPath = this.path.get_child(name);
+	    let match = VERSION_RE.exec(name);
+	    if (!match)
+		continue;
+	    this._history.push(new TaskHistoryEntry(childPath))
+	}
+	this._history.sort(TaskHistoryEntry.prototype.compareTo);
+    },
+
+    start: function() {
+	if (this._running) throw new Error();
+	this._running = true;
+	let yearver = new Date().getFullYear();
+	let lastversion = -1;
+	if (this._history.length > 0) {
+	    let last = this._history[this._history.length-1];
+	    if (last.major == yearver)
+		lastversion = last.minor;
+	    else
+		lastversion = -1;
+	}
+	let historyPath = this.path.get_child(format.vprintf('%d.%d', [yearver, lastversion + 1]));
+	GSystem.file_ensure_directory(historyPath, true, null);
+	let entry = new TaskHistoryEntry(historyPath, 'running');
+	this._history.push(entry);
+	entry.logfile_path = historyPath.get_child('log');
+	return entry;
+    },
+
+    finish: function(success) {
+	if (!this._running) throw new Error();
+	let last = this._history[this._history.length-1];
+	last.finish(success);
+	this._running = false;
+    },
+
+    getHistory: function() {
+	return this._history;
+    }
+});
diff --git a/src/ostbuild/ostbuild-js.in b/src/ostbuild/ostbuild-js.in
new file mode 100755
index 0000000..7011337
--- /dev/null
+++ b/src/ostbuild/ostbuild-js.in
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+jsdir= pkgdatadir@/js
+arg=$1
+shift
+
+export GI_TYPELIB_PATH="@pkglibdir@/girepository-1.0${GI_TYPELIB_PATH:+:GI_TYPELIB_PATH}"
+export LD_LIBRARY_PATH="@pkglibdir@/${LD_LIBRARY_PATH:+:LD_LIBRARY_PATH}"
+# Don't auto-spawn a session bus
+export GIO_USE_VFS=local
+
+exec gjs -I "${jsdir}" "${jsdir}/${arg}.js" "$@"
diff --git a/src/ostbuild/pyostbuild/main.py b/src/ostbuild/pyostbuild/main.py
index 1da65da..86c3423 100755
--- a/src/ostbuild/pyostbuild/main.py
+++ b/src/ostbuild/pyostbuild/main.py
@@ -22,7 +22,6 @@ import sys
 import argparse
 
 from . import builtins
-from . import builtin_autobuilder
 from . import builtin_build
 from . import builtin_checkout
 from . import builtin_deploy_root
@@ -37,12 +36,16 @@ from . import builtin_repoweb_json
 from . import builtin_resolve
 from . import builtin_source_diff
 
+JS_BUILTINS = {'autobuilder': 'Run resolve and build'}
+
 def usage(ecode):
     print "Builtins:"
     for builtin in builtins.get_all():
         if builtin.name.startswith('privhelper'):
             continue
         print "    %s - %s" % (builtin.name, builtin.short_description)
+    for name,short_description in JS_BUILTINS.items():
+        print "    %s - %s" % (name, short_description)
     return ecode
 
 def main(args):
@@ -51,10 +54,17 @@ def main(args):
     elif args[0] in ('-h', '--help'):
         return usage(0)
     else:
-        builtin = builtins.get(args[0])
+        name = args[0]
+        builtin = builtins.get(name)
         if builtin is None:
-            print "error: Unknown builtin '%s'" % (args[0], )
-            return usage(1)
+            js_builtin = JS_BUILTINS.get(name)
+            if js_builtin is None:
+                print "error: Unknown builtin '%s'" % (args[0], )
+                return usage(1)
+            else:
+                child_args = ['ostbuild-js', name]
+                child_args.extend(args[:1])
+                os.execvp('ostbuild-js', child_args)
         return builtin.execute(args[1:])
     
     



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