Import lots of code from gnome-continuous

This doesn't really do anything yet, but the idea is to start doing
Continuous-like things (such as actually booting the result).
This commit is contained in:
Colin Walters 2014-01-03 17:07:19 -05:00
parent 220773f213
commit 66a19b6475
34 changed files with 6173 additions and 36 deletions

View File

@ -22,54 +22,50 @@ substitutions= \
-e s,@pkgdatadir\@,$(pkgdatadir), \
$(NULL)
rpm-ostree-autobuilder: src/rpm-ostree-autobuilder.in Makefile
rpm-ostree-autobuilder: src/autobuilder/rpm-ostree-autobuilder.in Makefile
sed $(substitutions) $< > $@.tmp && mv $@.tmp $@
EXTRA_DIST += src/rpm-ostree-autobuilder.in
EXTRA_DIST += src/autobuilder/rpm-ostree-autobuilder.in
bin_SCRIPTS += rpm-ostree-autobuilder \
$(NULL)
jsautobuilderdir=$(pkgdatadir)/js
jsautobuilder_DATA= \
src/js/argparse.js \
src/js/asyncutil.js \
src/js/buildutil.js \
src/js/builtin.js \
src/js/fileutil.js \
src/js/task.js \
src/js/jsonutil.js \
src/js/jsutil.js \
src/js/main.js \
src/js/libqa.js \
src/js/guestfish.js \
src/js/params.js \
src/js/procutil.js \
src/js/snapshot.js \
src/js/streamutil.js \
src/js/vcs.js \
src/js/versioneddir.js \
src/autobuilder/js/argparse.js \
src/autobuilder/js/asyncutil.js \
src/autobuilder/js/buildutil.js \
src/autobuilder/js/builtin.js \
src/autobuilder/js/fileutil.js \
src/autobuilder/js/task.js \
src/autobuilder/js/jsonutil.js \
src/autobuilder/js/jsutil.js \
src/autobuilder/js/main.js \
src/autobuilder/js/libqa.js \
src/autobuilder/js/guestfish.js \
src/autobuilder/js/params.js \
src/autobuilder/js/procutil.js \
src/autobuilder/js/snapshot.js \
src/autobuilder/js/streamutil.js \
src/autobuilder/js/vcs.js \
src/autobuilder/js/versioneddir.js \
$(NULL)
jsautobuiltinsdir=$(jsautobuilderdir)/builtins
jsautobuiltins_DATA= \
src/js/builtins/autobuilder.js \
src/js/builtins/checkout.js \
src/js/builtins/git_mirror.js \
src/js/builtins/make.js \
src/js/builtins/qa_make_disk.js \
src/js/builtins/run_task.js \
src/js/builtins/shell.js \
src/autobuilder/js/builtins/autobuilder.js \
src/autobuilder/js/builtins/git_mirror.js \
src/autobuilder/js/builtins/make.js \
src/autobuilder/js/builtins/qa_make_disk.js \
src/autobuilder/js/builtins/run_task.js \
src/autobuilder/js/builtins/shell.js \
$(NULL)
jsautotasksdir=$(jsautobuilderdir)/tasks
jsautotasks_DATA= \
src/js/tasks/task-build.js \
src/js/tasks/task-bdiff.js \
src/js/tasks/task-builddisks.js \
src/js/tasks/task-smoketest.js \
src/js/tasks/task-integrationtest.js \
src/js/tasks/task-memusage.js \
src/js/tasks/task-applicationstest.js \
src/js/tasks/task-zdisks.js \
src/js/tasks/testbase.js \
src/autobuilder/js/tasks/task-build.js \
src/autobuilder/js/tasks/task-bdiff.js \
src/autobuilder/js/tasks/task-builddisks.js \
src/autobuilder/js/tasks/task-smoketest.js \
src/autobuilder/js/tasks/task-zdisks.js \
src/autobuilder/js/tasks/testbase.js \
$(NULL)

View File

@ -42,3 +42,5 @@ typelibdir = $(pkglibdir)/girepository-1.0
gir_DATA = $(INTROSPECTION_GIRS)
typelib_DATA = $(gir_DATA:.gir=.typelib)
include Makefile-autobuilder.am

@ -1 +1 @@
Subproject commit 4ea458abb5f0db4c2fd51b000a0a7b9e8f510721
Subproject commit 25f755e086282b17491038b08885d726dcf48303

View File

@ -0,0 +1,181 @@
// 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 ArgumentParser = new Lang.Class({
Name: 'ArgumentParser',
_init: function(description) {
this.description = description;
this._opts = [];
this._namedArgs = [];
this._optNames = {};
this._argNames = {};
},
usage: function() {
let buf = 'Usage: ' + this.description + '\n';
for (let i = 0; i < this._opts.length; i++) {
let opt = this._opts[i];
let names = opt._names;
for (let j = 0; j < names.length; j++) {
let name = names[j];
buf += name;
if (j < names.length - 1)
buf += ", ";
}
if (opt.description)
buf += ' ' + opt.description;
buf += '\n';
}
for (let i = 0; i < this._namedArgs.length; i++) {
let arg = this._namedArgs[i];
buf += arg._varName + "\n";
}
return buf;
},
addArgument: function(nameOrNames, opts) {
if (!opts)
opts = {};
let names;
if (nameOrNames instanceof Array)
names = nameOrNames;
else
names = [nameOrNames];
if (opts.action == undefined)
opts.action = 'store';
if (opts.nargs == undefined)
opts.nargs = '1';
if (names.length == 0) {
throw new Error("Must specify at least one argument");
} else if (names.length == 1 && names[0][0] != '-') {
let name = names[0];
this._namedArgs.push(opts);
this._argNames[name] = opts;
opts._varName = name;
} else {
opts._names = names;
this._opts.push(opts);
opts._varName = null;
let shortOpt = null;
for (let i = 0; i < names.length; i++) {
let name = names[i];
if (this._optNames[name]) {
throw new Error("Argument " + name + " already added");
} else if (names.length != 1 && name[0] != '-') {
throw new Error("Argument " + name + " does not start with -");
}
this._optNames[name] = opts;
if (opts._varName == null) {
if (name.indexOf('--') == 0)
opts._varName = name.substr(2).replace(/-/g, '_');
else if (shortOpt == null && name[0] == '-' && name[1] != '-')
shortOpt = name.substr(1);
}
}
if (opts._varName == null)
opts._varName = shortOpt;
}
},
_failed: function() {
print(this.usage());
throw new Error("Argument parsing failed");
},
parse: function(argv) {
let result = {};
let rest = [];
for (let name in this._optNames) {
let opts = this._optNames[name];
if (opts.action == 'store') {
result[opts._varName] = null;
} else if (opts.action == 'storeTrue') {
result[opts._varName] = false;
} else if (opts.action == 'append') {
result[opts._varName] = [];
}
}
for (let name in this._argNames) {
let opts = this._argNames[name];
if (opts.nargs == '*')
result[name] = [];
else
result[name] = null;
}
let rest = [];
for (let i = 0; i < argv.length; i++) {
let arg = argv[i];
if (arg[0] == '-') {
let equalsIdx = arg.indexOf('=');
let opts;
if (equalsIdx != -1)
opts = this._optNames[arg.substr(0, equalsIdx)];
else
opts = this._optNames[arg];
if (!opts) this._failed();
if (opts.action == 'store' || opts.action == 'append') {
let val;
if (equalsIdx == -1) {
if (i == argv.length - 1) this._failed();
val = argv[i+1];
i++;
} else {
val = arg.substr(equalsIdx+1);
}
if (opts.action == 'store')
result[opts._varName] = val;
else
result[opts._varName].push(val);
} else if (opts.action == 'storeTrue') {
result[opts._varName] = true;
}
} else {
rest.push(arg);
}
}
for (let i = 0; i < this._namedArgs.length; i++) {
let opts = this._namedArgs[i];
let varName = opts._varName;
if (opts.nargs == '*') {
result[varName].push.apply(result[varName], rest);
} else {
if (rest.length == 0) this._failed();
let value = rest.shift();
result[varName] = value;
}
}
return result;
}
});

View File

@ -0,0 +1,71 @@
// 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 AsyncSet = new Lang.Class({
Name: 'AsyncSet',
_init: function(callback, cancellable) {
this._callback = callback;
this._cancellable = cancellable;
this._results = {};
this._err = null;
this._children = [];
},
addGAsyncResult: function(name, callback) {
this._children.push(callback);
let wrapped = Lang.bind(this, function(object, result) {
let success = false;
try {
this._results[name] = callback(object, result);
success = true;
} catch (e) {
if (this._cancellable)
this._cancellable.cancel();
if (!this._err) {
this._err = e.toString();
this._checkCallback();
return;
}
}
let i;
for (i = 0; i < this._children.length; i++) {
let child = this._children[i];
if (child === callback) {
break;
}
}
if (i == this._children.length)
throw new Error("Failed to find child task");
this._children.splice(i, 1);
this._checkCallback();
});
return wrapped;
},
_checkCallback: function() {
if (this._err)
this._callback(null, this._err);
else if (this._children.length == 0)
this._callback(this._results, null);
}
});

View File

@ -0,0 +1,137 @@
// 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 GSystem = imports.gi.GSystem;
const BUILD_ENV = {
'HOME' : '/',
'HOSTNAME' : 'ostbuild',
'LANG': 'C',
'PATH' : '/usr/bin:/bin:/usr/sbin:/sbin',
'SHELL' : '/bin/bash',
'TERM' : 'vt100',
'TMPDIR' : '/tmp',
'TZ': 'EST5EDT'
};
function getPatchPathsForComponent(patchdir, component) {
let patches = component['patches'];
if (!patches)
return [];
let patchSubdir = patches['subdir'];
let subPatchdir;
if (patchSubdir) {
subPatchdir = patchdir.get_child(patchSubdir);
} else {
subPatchdir = patchdir;
}
let result = [];
let files = patches['files'];
for (let i = 0; i < files.length; i++) {
result.push(subPatchdir.get_child(files[i]));
}
return result;
}
function findUserChrootPath() {
// We need to search PATH here manually so we correctly pick up an
// ostree install in e.g. ~/bin even though we're going to set PATH
// below for our children inside the chroot.
let userChrootPath = null;
let elts = GLib.getenv('PATH').split(':');
for (let i = 0; i < elts.length; i++) {
let dir = Gio.File.new_for_path(elts[i]);
let child = dir.get_child('linux-user-chroot');
if (child.query_exists(null)) {
userChrootPath = child;
break;
}
}
return userChrootPath;
}
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 = GSystem.file_get_relpath(parent, newTarget);
tmpLinkPath.make_symbolic_link(relpath, cancellable);
GSystem.file_rename(tmpLinkPath, linkPath, cancellable);
}
function checkIsWorkDirectory(dir) {
let manifest = dir.get_child('manifest.json');
if (!manifest.query_exists(null)) {
throw new Error("No manifest.json found in " + dir.get_path());
}
let dotGit = dir.get_child('.git');
if (dotGit.query_exists(null)) {
throw new Error(".git found in " + dir.get_path() + "; are you in a gnome-ostree checkout?");
}
}
function formatElapsedTime(microseconds) {
let millis = microseconds / 1000;
if (millis > 1000) {
let seconds = millis / 1000;
return Format.vprintf("%.1f s", [seconds]);
}
return Format.vprintf("%.1f ms", [millis]);
}
function timeSubtask(name, cb) {
let startTime = GLib.get_monotonic_time();
cb();
let endTime = GLib.get_monotonic_time();
print("Subtask " + name + " complete in " + formatElapsedTime(endTime - startTime));
}

View File

@ -0,0 +1,67 @@
// 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 GSystem = imports.gi.GSystem;
const Params = imports.params;
const JsonUtil = imports.jsonutil;
const ArgParse = imports.argparse;
const Snapshot = imports.snapshot;
const BuildUtil = imports.buildutil;
const Builtin = new Lang.Class({
Name: 'Builtin',
DESCRIPTION: null,
_init: function() {
this.parser = new ArgParse.ArgumentParser(this.DESCRIPTION);
this._workdirInitialized = false;
},
_initWorkdir: function(workdir, cancellable) {
if (this._workdirInitialized)
return;
this._workdirInitialized = true;
if (workdir === null)
workdir = Gio.File.new_for_path('.');
else if (typeof(workdir) == 'string')
workdir = Gio.File.new_for_path(workdir);
BuildUtil.checkIsWorkDirectory(workdir);
this.workdir = workdir;
this.mirrordir = workdir.get_child('src');
GSystem.file_ensure_directory(this.mirrordir, true, cancellable);
this.patchdir = this.workdir.get_child('patches');
},
_initSnapshot: function(workdir, snapshotPath, cancellable) {
this._initWorkdir(workdir, cancellable);
let path = Gio.File.new_for_path(snapshotPath);
this._snapshot = Snapshot.fromFile(path, cancellable);
},
main: function(argv, loop, cancellable) {
let args = this.parser.parse(argv);
this.execute(args, loop, cancellable);
}
});

View File

@ -0,0 +1,209 @@
// 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 GSystem = imports.gi.GSystem;
const Builtin = imports.builtin;
const Task = imports.task;
const JsonUtil = imports.jsonutil;
const BuildUtil = imports.buildutil;
const ProcUtil = imports.procutil;
const VersionedDir = imports.versioneddir;
var AutoBuilderIface = '<node> \
<interface name="org.gnome.OSTreeBuild.AutoBuilder"> \
<method name="queueResolve"> \
<arg type="as" direction="in" /> \
</method> \
<property name="Status" type="s" access="read" /> \
</interface> \
</node>';
const Autobuilder = new Lang.Class({
Name: 'Autobuilder',
Extends: Builtin.Builtin,
DESCRIPTION: "Automatically fetch git repositories and build",
_init: function() {
this.parent();
this.parser.addArgument('--autoupdate-self', { action: 'store' });
this._buildNeeded = true;
this._initialResolveNeeded = true;
this._fullResolveNeeded = true;
this._resolveTimeout = 0;
this._resolveSrcUrls = [];
},
execute: function(args, loop, cancellable) {
this._initWorkdir(null, cancellable);
this._buildsDir = new VersionedDir.VersionedDir(this.workdir.get_child('builds'));
this._resultsDir = this.workdir.get_child('results');
GSystem.file_ensure_directory(this._resultsDir, true, cancellable);
if (args.autoupdate_self)
this._autoupdate_self = Gio.File.new_for_path(args.autoupdate_self);
this._ownId = Gio.DBus.session.own_name('org.gnome.OSTreeBuild', Gio.BusNameOwnerFlags.NONE,
function(name) {},
function(name) { loop.quit(); });
this._impl = Gio.DBusExportedObject.wrapJSObject(AutoBuilderIface, this);
this._impl.export(Gio.DBus.session, '/org/gnome/OSTreeBuild/AutoBuilder');
this._taskmaster = new Task.TaskMaster(this.workdir);
this._taskmaster.connect('task-executing', Lang.bind(this, this._onTaskExecuting));
this._taskmaster.connect('task-complete', Lang.bind(this, this._onTaskCompleted));
/* Start an initial, non-fetching resolve */
this._runResolve();
/* Flag immediately that we need a full resolve */
this._fullResolveNeeded = true;
/* And set a timeout for 10 minutes for the next full resolve */
this._resolveTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
60 * 10, Lang.bind(this, this._triggerFullResolve));
this._updateStatus();
loop.run();
},
_onTaskExecuting: function(taskmaster, task) {
print("Task " + task.name + " executing on " + task.buildName);
this._updateStatus();
},
_onTaskCompleted: function(taskmaster, task, success, error) {
let cancellable = null;
if (task.name == 'resolve') {
if (!task.changed) {
print("Resolve is unchanged");
this._buildsDir.deleteCurrentVersion(null);
}
this._runResolve();
}
let resultsPath;
if (success) {
print("Task " + task.name + " complete: " + task.buildName);
resultsPath = this._resultsDir.get_child('successful');
} else {
print("Task " + task.name + " failed: " + task.buildName);
resultsPath = this._resultsDir.get_child('failed');
}
BuildUtil.atomicSymlinkSwap(resultsPath, task.buildPath, null);
this._updateStatus();
},
_updateStatus: function() {
let newStatus = "";
let taskstateList = this._taskmaster.getTaskState();
let runningTasks = [];
let queuedTasks = [];
for (let i = 0; i < taskstateList.length; i++) {
let taskstate = taskstateList[i];
let name = taskstate.task.name;
if (taskstate.running)
runningTasks.push(name);
else
queuedTasks.push(name);
}
if (runningTasks.length == 0 && queuedTasks.length == 0) {
newStatus = "[idle]";
} else {
newStatus = "running: " + JSON.stringify(runningTasks);
if (queuedTasks.length)
newStatus += " queued: " + JSON.stringify(queuedTasks);
}
if (newStatus != this._status) {
this._status = newStatus;
print(this._status);
let [success,loadAvg,etag] = Gio.File.new_for_path('/proc/loadavg').load_contents(null);
loadAvg = loadAvg.toString().replace(/\n$/, '').split(' ');
let statusPath = Gio.File.new_for_path('autobuilder-status.json');
JsonUtil.writeJsonFileAtomic(statusPath, {'running': runningTasks,
'queued': queuedTasks,
'systemLoad': loadAvg}, null);
this._impl.emit_property_changed('Status', new GLib.Variant("s", this._status));
}
},
get Status() {
return this._status;
},
queueResolve: function(srcUrls) {
print("Queuing force resolve for " + JSON.stringify(srcUrls));
this._resolveSrcUrls.push.apply(this._resolveSrcUrls, srcUrls);
this._runResolve();
},
_triggerFullResolve: function() {
this._fullResolveNeeded = true;
this._runResolve();
return true;
},
_runResolve: function() {
let cancellable = null;
if (!(this._initialResolveNeeded ||
this._resolveSrcUrls.length > 0 ||
this._fullResolveNeeded))
return;
if (this._taskmaster.isTaskQueued('resolve'))
return;
if (this._autoupdate_self)
ProcUtil.runSync(['git', 'pull', '-r'], cancellable,
{ cwd: this._autoupdate_self })
let previousVersion = this._buildsDir.currentVersion(cancellable);
let buildPath = this._buildsDir.allocateNewVersion(cancellable);
let version = this._buildsDir.pathToVersion(buildPath);
if (previousVersion) {
let lastBuildPath = this._buildsDir.createPathForVersion(previousVersion);
BuildUtil.atomicSymlinkSwap(buildPath.get_child('last-build'), lastBuildPath, null);
}
if (this._initialResolveNeeded) {
this._initialResolveNeeded = false;
this._taskmaster.pushTask(buildPath, 'resolve', { });
} else if (this._fullResolveNeeded) {
this._fullResolveNeeded = false;
this._taskmaster.pushTask(buildPath, 'resolve', { fetchAll: true });
} else {
this._taskmaster.pushTask(buildPath, 'resolve', { fetchSrcUrls: this._resolveSrcUrls });
}
this._resolveSrcUrls = [];
this._updateStatus();
}
});

View File

@ -0,0 +1,78 @@
// Copyright (C) 2011,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.
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const Builtin = imports.builtin;
const Snapshot = imports.snapshot;
const Vcs = imports.vcs;
const JsonUtil = imports.jsonutil;
const GitMirror = new Lang.Class({
Name: 'GitMirror',
Extends: Builtin.Builtin,
DESCRIPTION: "Update internal git mirror for one or more components",
_init: function() {
this.parent();
this.parser.addArgument('--workdir');
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',
help: "Don't exit on fetch failures"});
this.parser.addArgument('components', {nargs:'*'});
},
execute: function(args, loop, cancellable) {
this._initWorkdir(args.workdir, cancellable);
if (!args.timeout_sec)
args.timeout_sec = 0;
if (args.manifest != null) {
let manifestPath = Gio.File.new_for_path(args.manifest)
this._snapshot = Snapshot.fromFile(manifestPath, cancellable, { prepareResolve: true });
} else {
this._initSnapshot(null, args.snapshot, cancellable);
}
let componentNames;
if (args.components.length == 0) {
componentNames = this._snapshot.getAllComponentNames();
} else {
componentNames = args.components;
}
componentNames.forEach(Lang.bind(this, function (name) {
let component = this._snapshot.getComponent(name);
if (!args.fetch) {
Vcs.ensureVcsMirror(this.mirrordir, component, cancellable);
} else {
print("Running git fetch for " + name);
Vcs.fetch(this.mirrordir, component, cancellable,
{ keepGoing:args.keep_going,
timeoutSec: args.timeout_sec });
}
}));
}
});

View File

@ -0,0 +1,103 @@
// 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 GSystem = imports.gi.GSystem;
const Builtin = imports.builtin;
const Task = imports.task;
const Make = new Lang.Class({
Name: 'Make',
Extends: Builtin.Builtin,
DESCRIPTION: "Execute tasks",
_init: function() {
this.parent();
this.parser.addArgument(['-n', '--only'], { action: 'storeTrue',
help: "Don't process tasks after this" });
this.parser.addArgument(['-x', '--skip'], { action: 'append',
help: "Don't process tasks after this" });
this.parser.addArgument('taskname');
this.parser.addArgument('parameters', { nargs: '*' });
},
execute: function(args, loop, cancellable) {
this._initWorkdir(null, cancellable);
this._loop = loop;
this._failed = false;
this._cancellable = cancellable;
this._oneOnly = args.only;
let taskmaster = new Task.TaskMaster(this.workdir,
{ onEmpty: Lang.bind(this, this._onTasksComplete),
processAfter: !args.only,
skip: args.skip });
this._taskmaster = taskmaster;
taskmaster.connect('task-executing', Lang.bind(this, this._onTaskExecuting));
taskmaster.connect('task-complete', Lang.bind(this, this._onTaskCompleted));
let params = this._parseParameters(args.parameters);
let buildPath = Gio.File.new_for_path('local');
GSystem.file_ensure_directory(buildPath, false, cancellable);
taskmaster.pushTask(buildPath, args.taskname, params);
loop.run();
if (!this._failed)
print("Success!")
},
_parseParameters: function(paramStrings) {
let params = {};
for (let i = 0; i < paramStrings.length; i++) {
let param = paramStrings[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;
}
return params;
},
_onTaskExecuting: function(taskmaster, task) {
print("Task " + task.name + " executing on " + task.buildName);
let output = task.taskCwd.get_child('output.txt');
if (this._oneOnly) {
let context = new GSystem.SubprocessContext({ argv: ['tail', '-f', output.get_path() ] });
this._tail = new GSystem.Subprocess({ context: context });
this._tail.init(null);
}
},
_onTaskCompleted: function(taskmaster, task, success, error) {
if (this._oneOnly)
this._tail.request_exit();
if (success) {
print("Task " + task.name + " complete: " + task.buildName);
} else {
this._failed = true;
print("Task " + task.name + " failed: " + task.buildName);
}
},
_onTasksComplete: function() {
this._loop.quit();
}
});

View File

@ -0,0 +1,53 @@
// -*- 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 ProcUtil = imports.procutil;
const GuestFish = imports.guestfish;
const QaMakeDisk = new Lang.Class({
Name: 'QaMakeDisk',
Extends: Builtin.Builtin,
DESCRIPTION: "Generate a disk image",
_init: function() {
this.parent();
this.parser.addArgument('diskpath');
},
execute: function(args, loop, cancellable) {
let path = Gio.File.new_for_path(args.diskpath);
if (path.query_exists(null))
throw new Error("" + path.get_path() + " exists");
let tmppath = path.get_parent().get_child(path.get_basename() + '.tmp');
GSystem.shutil_rm_rf(tmppath, cancellable);
LibQA.createDisk(tmppath, cancellable);
GSystem.file_rename(tmppath, path, cancellable);
print("Created: " + path.get_path());
}
});

View File

@ -0,0 +1,42 @@
// 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 Lang = imports.lang;
const Builtin = imports.builtin;
const Task = imports.task;
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 = taskset.getTask(args.taskName);
let params = JSON.parse(args.parameters);
let instance = new taskDef(params);
instance.execute(cancellable);
}
});

View File

@ -0,0 +1,31 @@
// 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 Lang = imports.lang;
const Builtin = imports.builtin;
const Shell = new Lang.Class({
Name: 'Shell',
Extends: Builtin.Builtin,
DESCRIPTION: "Interactive shell",
execute: function(args, loop, cancellable) {
imports.console.interact();
}
});

View File

@ -0,0 +1,89 @@
// 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 Params = imports.params;
function walkDirInternal(dir, matchParams, callback, cancellable, queryStr, depth, sortByName) {
let denum = dir.enumerate_children(queryStr, Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
cancellable);
let info;
let subdirs = [];
if (depth > 0) {
depth -= 1;
}
let sortedFiles = [];
while ((info = denum.next_file(cancellable)) != null) {
let name = info.get_name();
let child = dir.get_child(name);
let ftype = info.get_file_type();
if (depth != 0) {
if (ftype == Gio.FileType.DIRECTORY) {
subdirs.push(child);
continue;
}
}
if (matchParams.nameRegex && matchParams.nameRegex.exec(name) === null)
continue;
if (matchParams.fileType !== null && matchParams.fileType != info.get_file_type())
continue;
if (matchParams.contentType != null && matchParams.contentType != info.get_content_type())
continue;
if (!sortByName)
callback(child, cancellable);
else
sortedFiles.push(child);
}
if (sortByName) {
sortedFiles.sort(function (a, b) {
return a.get_basename().localeCompare(b.get_basename());
});
for (let i = 0; i < sortedFiles.length; i++) {
callback(sortedFiles[i], cancellable);
}
}
denum.close(cancellable);
if (sortByName) {
subdirs.sort(function (a, b) {
return a.get_basename().localeCompare(b.get_basename());
});
}
for (let i = 0; i < subdirs.length; i++) {
walkDirInternal(subdirs[i], matchParams, callback, cancellable, queryStr, depth);
}
}
function walkDir(dir, matchParams, callback, cancellable) {
matchParams = Params.parse(matchParams, { nameRegex: null,
fileType: null,
contentType: null,
depth: -1,
sortByName: false });
let queryStr = 'standard::name,standard::type,unix::mode';
if (matchParams.contentType)
queryStr += ',standard::fast-content-type';
let depth = matchParams.depth;
walkDirInternal(dir, matchParams, callback, cancellable, queryStr, depth, matchParams.sortByName);
}

View File

@ -0,0 +1,180 @@
// 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 GSystem = imports.gi.GSystem;
const Params = imports.params;
const ProcUtil = imports.procutil;
const LibGuestfs = new Lang.Class({
Name: 'LibGuestfs',
_init: function(diskpath, params) {
this._params = Params.parse(params, {useLockFile: true,
partitionOpts: ['-i'],
readWrite: false});
this._diskpath = diskpath;
this._readWrite = params.readWrite
this._partitionOpts = params.partitionOpts;
if (params.useLockFile) {
let lockfilePath = diskpath.get_parent().get_child(diskpath.get_basename() + '.guestfish-lock');
this._lockfilePath = lockfilePath;
} else {
this._lockfilePath = null;
}
},
_lock: function() {
if (this._lockfilePath) {
let stream = this._lockfilePath.create(Gio.FileCreateFlags.NONE, cancellable);
stream.close(cancellable);
}
},
_unlock: function() {
if (this._lockfilePath != null) {
GSystem.file_unlink(this._lockfilePath, cancellable);
}
},
_appendOpts: function(argv) {
argv.push.apply(argv, ['-a', this._diskpath.get_path()]);
if (this._readWrite)
argv.push('--rw');
else
argv.push('--ro');
argv.push.apply(argv, this._partitionOpts);
}
});
const GuestFish = new Lang.Class({
Name: 'GuestFish',
Extends: LibGuestfs,
run: function(input, cancellable) {
this._lock();
try {
let guestfishArgv = ['guestfish'];
this._appendOpts(guestfishArgv);
return ProcUtil.runProcWithInputSyncGetLines(guestfishArgv, cancellable, input);
} finally {
this._unlock();
}
}
});
const GuestMount = new Lang.Class({
Name: 'GuestMount',
Extends: LibGuestfs,
mount: function(mntdir, cancellable) {
this._lock();
try {
this._mntdir = mntdir;
this._mountPidFile = mntdir.get_parent().get_child(mntdir.get_basename() + '.guestmount-pid');
if (this._mountPidFile.query_exists(null))
throw new Error("guestfish pid file exists: " + this._mountPidFile.get_path());
let guestmountArgv = ['guestmount', '-o', 'allow_root',
'--pid-file', this._mountPidFile.get_path()];
this._appendOpts(guestmountArgv);
guestmountArgv.push(mntdir.get_path());
print('Mounting ' + mntdir.get_path() + ' : ' + guestmountArgv.join(' '));
let context = new GSystem.SubprocessContext({ argv: guestmountArgv });
let proc = new GSystem.Subprocess({ context: context });
proc.init(cancellable);
// guestfish daemonizes, so this process will exit, and
// when it has, we know the mount is ready. If there was
// a way to get notified instead of this indirect fashion,
// we'd do that.
proc.wait_sync_check(cancellable);
this._mounted = true;
} catch (e) {
this._unlock();
}
},
umount: function(cancellable) {
if (!this._mounted)
return;
let pidStr = GSystem.file_load_contents_utf8(this._mountPidFile, cancellable);
if (pidStr.length == 0) {
this._mounted = false;
return;
}
for (let i = 0; i < 30; i++) {
// See "man guestmount" for why retry loops here might be needed if this
// script is running on a client machine with programs that watch for new mounts
try {
ProcUtil.runSync(['fusermount', '-u', this._mntdir.get_path()], cancellable,
{logInitiation: true});
break;
} catch (e) {
if (!(e.origError && e.origError.domain == GLib.spawn_exit_error_quark()))
throw e;
else {
let proc = GSystem.Subprocess.new_simple_argv(['fuser', '-m', this._mntdir.get_path()],
GSystem.SubprocessStreamDisposition.INHERIT,
GSystem.SubprocessStreamDisposition.INHERIT,
cancellable);
proc.init(cancellable);
proc.wait_sync(cancellable);
let creds = new Gio.Credentials();
proc = GSystem.Subprocess.new_simple_argv(['ls', '-al', '/proc/' + creds.get_unix_pid() + '/fd'],
GSystem.SubprocessStreamDisposition.INHERIT,
GSystem.SubprocessStreamDisposition.INHERIT,
cancellable);
proc.init(cancellable);
proc.wait_sync(cancellable);
GLib.usleep(GLib.USEC_PER_SEC);
}
}
}
let pid = parseInt(pidStr);
let guestfishExited = false;
let guestfishTimeoutSecs = 5 * 60;
for (let i = 0; i < guestfishTimeoutSecs; i++) {
let killContext = new GSystem.SubprocessContext({argv: ['kill', '-0', ''+pid]});
killContext.set_stderr_disposition(GSystem.SubprocessStreamDisposition.NULL);
let killProc = new GSystem.Subprocess({context: killContext});
killProc.init(null);
let [waitSuccess, ecode] = killProc.wait_sync(null);
let [killSuccess, statusStr] = ProcUtil.getExitStatusAndString(ecode);
if (killSuccess) {
print("Awaiting termination of guestfish, pid=" + pid + " timeout=" + (guestfishTimeoutSecs - i) + "s");
GLib.usleep(GLib.USEC_PER_SEC);
} else {
guestfishExited = true;
break;
}
}
if (!guestfishExited)
throw new Error("guestfish failed to exit");
this._mounted = false;
this._unlock();
},
});

View File

@ -0,0 +1,82 @@
// 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;
/* jsonutil.js:
* Read/write JSON to/from GFile paths, very inefficiently.
*/
function serializeJson(data) {
return JSON.stringify(data, null, " ");
}
function writeJsonToStream(stream, data, cancellable) {
let buf = serializeJson(data);
stream.write_bytes(new GLib.Bytes(buf), cancellable);
}
function writeJsonToStreamAsync(stream, data, cancellable, onComplete) {
let buf = serializeJson(data);
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);
s.close(cancellable);
}
function loadJson(path, cancellable) {
let [success,contents,etag] = path.load_contents(cancellable);
return JSON.parse(contents);
}

View File

@ -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;
}

407
src/autobuilder/js/libqa.js Normal file
View File

@ -0,0 +1,407 @@
// 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 OSTree = imports.gi.OSTree;
const GSystem = imports.gi.GSystem;
const Params = imports.params;
const ProcUtil = imports.procutil;
const GuestFish = imports.guestfish;
const DEFAULT_GF_PARTITION_OPTS = ['-m', '/dev/sda3', '-m', '/dev/sda1:/boot'];
function linuxGetMemTotalMb() {
let [success,contents] = GLib.file_get_contents('/proc/meminfo');
let contentLines = contents.toString().split(/\n/);
for (let i = 0; contentLines.length; i++) {
let line = contentLines[i];
if (line.indexOf('MemTotal:') == 0) {
return parseInt(/([0-9]+) kB/.exec(line)[1]) / 1024;
}
}
throw new Error("Couldn't determine total memory from /proc/meminfo");
}
function getQemuPath() {
let fallbackPaths = ['/usr/libexec/qemu-kvm']
let qemuPathString = GLib.find_program_in_path('qemu-kvm');
qemuPathString = GLib.find_program_in_path('qemu-kvm');
if (!qemuPathString)
qemuPathString = GLib.find_program_in_path('kvm');
if (qemuPathString == null) {
for (let i = 0; i < fallbackPaths.length; i++) {
let path = Gio.File.new_for_path(fallbackPaths[i]);
if (!path.query_exists(null))
continue;
qemuPathString = path.get_path();
}
}
if (qemuPathString == null) {
throw new Error("Unable to find qemu-kvm");
}
return qemuPathString;
}
function getDefaultQemuOptions(params) {
params = Params.parse(params, { parallel: false });
let args = [getQemuPath(), '-vga', 'std', '-usb', '-usbdevice', 'tablet', '-net', 'none'];
let systemMemoryMb = linuxGetMemTotalMb();
let minimumGuestMemoryMb = 768;
let maximumGuestMemoryMb = 4 * 1024;
// As a guess, use 1/4 of host memory, rounded up to the nearest
// multiple of 128M; subject to above constraints as a lame
// default...we need global coordination here.
let guestMemoryGuessMb = Math.floor(systemMemoryMb / 4 / 128) * 128;
let guestMemory = Math.floor(Math.max(minimumGuestMemoryMb,
Math.min(maximumGuestMemoryMb,
guestMemoryGuessMb)));
args.push.apply(args, ['-m', ''+guestMemory]);
if (params.parallel) {
let nCores = Math.min(16, GLib.get_num_processors());
args.push.apply(args, ['-smp', ''+nCores]);
}
return args;
}
function newReadWriteMount(diskpath, cancellable) {
let mntdir = Gio.File.new_for_path('mnt');
GSystem.file_ensure_directory(mntdir, true, cancellable);
let gfmnt = new GuestFish.GuestMount(diskpath, {partitionOpts: DEFAULT_GF_PARTITION_OPTS,
readWrite: true});
gfmnt.mount(mntdir, cancellable);
return [gfmnt, mntdir];
}
function createDisk(diskpath, cancellable) {
let sizeMb = 8 * 1024;
let bootsizeMb = 200;
let swapsizeMb = 64;
let guestfishProcess;
ProcUtil.runSync(['qemu-img', 'create', '-f', 'qcow2', diskpath.get_path(), '' + sizeMb + 'M'], cancellable);
let makeDiskCmd = 'launch\n\
part-init /dev/sda mbr\n\
blockdev-getsize64 /dev/sda\n\
blockdev-getss /dev/sda\n';
let gf = new GuestFish.GuestFish(diskpath, {partitionOpts: [], readWrite: true});
let lines = gf.run(makeDiskCmd, cancellable);
if (lines.length != 2)
throw new Error("guestfish returned unexpected output lines (" + lines.length + ", expected 2");
let diskBytesize = parseInt(lines[0]);
let diskSectorsize = parseInt(lines[1]);
print(Format.vprintf("bytesize: %s sectorsize: %s", [diskBytesize, diskSectorsize]));
let bootsizeSectors = bootsizeMb * 1024 / diskSectorsize * 1024;
let swapsizeSectors = swapsizeMb * 1024 / diskSectorsize * 1024;
let rootsizeSectors = diskBytesize / diskSectorsize - bootsizeSectors - swapsizeSectors - 64;
let bootOffset = 64;
let swapOffset = bootOffset + bootsizeSectors;
let rootOffset = swapOffset + swapsizeSectors;
let endOffset = rootOffset + rootsizeSectors;
let partconfig = Format.vprintf('launch\n\
part-add /dev/sda p %s %s\n\
part-add /dev/sda p %s %s\n\
part-add /dev/sda p %s %s\n\
part-set-bootable /dev/sda 1 true\n\
mkfs ext4 /dev/sda1\n\
set-e2label /dev/sda1 gnostree-boot\n\
mkswap-L gnostree-swap /dev/sda2\n\
mkfs ext4 /dev/sda3\n\
set-e2label /dev/sda3 gnostree-root\n\
mount /dev/sda3 /\n\
mkdir /boot\n\
', [bootOffset, swapOffset - 1,
swapOffset, rootOffset - 1,
rootOffset, endOffset - 1]);
print("partition config: ", partconfig);
gf.run(partconfig, cancellable);
}
function createDiskSnapshot(diskpath, newdiskpath, cancellable) {
ProcUtil.runSync(['qemu-img', 'create', '-f', 'qcow2', '-o', 'backing_file=' + diskpath.get_path(),
newdiskpath.get_path()], cancellable);
}
function copyDisk(srcpath, destpath, cancellable) {
ProcUtil.runSync(['qemu-img', 'convert', '-O', 'qcow2', srcpath.get_path(),
destpath.get_path()], cancellable);
}
function getSysrootAndCurrentDeployment(mntdir, osname) {
let sysroot = OSTree.Sysroot.new(mntdir);
sysroot.load(null);
let deployments = sysroot.get_deployments().filter(function (deployment) {
return deployment.get_osname() == osname;
});
if (deployments.length == 0)
throw new Error("No deployments for " + osname + " in " + mntdir.get_path());
let current = deployments[0];
return [sysroot, current];
}
function getDeployDirs(mntdir, osname) {
let [sysroot, current] = getSysrootAndCurrentDeployment(mntdir, osname);
let deployDir = sysroot.get_deployment_directory(current);
return [deployDir, deployDir.get_child('etc')];
}
function modifyBootloaderAppendKernelArgs(mntdir, kernelArgs, cancellable) {
let confPath = mntdir.resolve_relative_path('boot/syslinux/syslinux.cfg');
let conf = GSystem.file_load_contents_utf8(confPath, cancellable);
let lines = conf.split('\n');
let modifiedLines = [];
let didModify = false;
let kernelArg = kernelArgs.join(' ');
let kernelLineRe = /\tAPPEND /;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let match = kernelLineRe.exec(line);
if (!match) {
modifiedLines.push(line);
} else {
modifiedLines.push(line + ' ' + kernelArg);
didModify = true;
}
}
if (!didModify)
throw new Error("Failed to find APPEND option in syslinux.cfg");
let modifiedConf = modifiedLines.join('\n');
confPath.replace_contents(modifiedConf, null, false,
Gio.FileCreateFlags.NONE,
cancellable);
}
function getMultiuserWantsDir(currentEtcDir) {
return currentEtcDir.resolve_relative_path('systemd/system/multi-user.target.wants');
}
function getDatadir() {
return Gio.File.new_for_path(GLib.getenv('OSTBUILD_DATADIR'));
}
function injectExportJournal(currentDir, currentEtcDir, cancellable) {
let binDir = currentDir.resolve_relative_path('usr/bin');
let multiuserWantsDir = getMultiuserWantsDir(currentEtcDir);
let datadir = getDatadir();
let exportScript = datadir.resolve_relative_path('tests/gnome-ostree-export-journal-to-serialdev');
let exportScriptService = datadir.resolve_relative_path('tests/gnome-ostree-export-journal-to-serialdev.service');
let exportBin = binDir.get_child(exportScript.get_basename());
exportScript.copy(exportBin, Gio.FileCopyFlags.OVERWRITE, cancellable, null, null);
GSystem.file_chmod(exportBin, 493, cancellable);
exportScriptService.copy(multiuserWantsDir.get_child(exportScriptService.get_basename()), Gio.FileCopyFlags.OVERWRITE, cancellable, null, null);
let journalConfPath = currentEtcDir.resolve_relative_path('systemd/journald.conf');
journalConfPath.replace_contents('[Journal]\n\
RateLimitInterval=0\n', null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
}
function injectTestUserCreation(currentDir, currentEtcDir, username, params, cancellable) {
params = Params.parse(params, { password: null });
let execLine;
if (params.password === null) {
execLine = Format.vprintf('/bin/sh -c "/usr/sbin/useradd %s; passwd -d %s"',
[username, username]);
} else {
execLine = Format.vprintf('/bin/sh -c "/usr/sbin/useradd %s; echo %s | passwd --stdin %s',
[username, params.password, username]);
}
let addUserService = '[Unit]\n\
Description=Add user %s\n\
Before=gdm.service\n\
[Service]\n\
ExecStart=%s\n\
Type=oneshot\n';
addUserService = Format.vprintf(addUserService, [username, execLine]);
let addUserServicePath = getMultiuserWantsDir(currentEtcDir).get_child('gnome-ostree-add-user-' + username + '.service');
addUserServicePath.replace_contents(addUserService, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
}
function enableAutologin(currentDir, currentEtcDir, username, cancellable) {
let gdmCustomPath = currentEtcDir.resolve_relative_path('gdm/custom.conf');
let keyfile = new GLib.KeyFile();
keyfile.load_from_file(gdmCustomPath.get_path(), GLib.KeyFileFlags.NONE);
keyfile.set_string('daemon', 'AutomaticLoginEnable', 'true');
keyfile.set_string('daemon', 'AutomaticLogin', username);
gdmCustomPath.replace_contents(keyfile.to_data()[0], null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
}
function _findFirstFileMatching(dir, prefix, cancellable) {
let d = dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
let finfo;
try {
while ((finfo = d.next_file(cancellable)) != null) {
let name = finfo.get_name();
if (name.indexOf(prefix) == 0) {
return dir.get_child(name);
}
}
throw new Error("Couldn't find " + prefix + " in " + dir.get_path());
} finally {
d.close(null);
}
return null;
}
function _findCurrentKernel(mntdir, osname, cancellable) {
let [sysroot, current] = getSysrootAndCurrentDeployment(mntdir, osname);
let deployBootdir = sysroot.get_deployment_directory(current).resolve_relative_path('boot');
return [_findFirstFileMatching(deployBootdir, 'vmlinuz-', cancellable),
_findFirstFileMatching(deployBootdir, 'initramfs-', cancellable)];
};
function _findCurrentOstreeBootArg(mntdir, cancellable) {
let bootLoaderEntriesDir = mntdir.resolve_relative_path('boot/loader/entries');
let conf = _findFirstFileMatching(bootLoaderEntriesDir, 'ostree-', cancellable);
let contents = GSystem.file_load_contents_utf8(conf, cancellable);
let lines = contents.split('\n');
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (line.indexOf('options ') != 0)
continue;
let options = line.substr(8).split(' ');
for (let j = 0; j < options.length; j++) {
let opt = options[j];
if (opt.indexOf('ostree=') != 0)
continue;
return opt;
}
}
throw new Error("Failed to find ostree= kernel argument");
}
function pullDeploy(mntdir, srcrepo, osname, target, revision, originRepoUrl, cancellable) {
let ostreedir = mntdir.get_child('ostree');
let ostreeOsdir = ostreedir.resolve_relative_path('deploy/' + osname);
let adminCmd = ['ostree', 'admin', '--sysroot=' + mntdir.get_path()];
let adminEnv = GLib.get_environ();
adminEnv.push('LIBGSYSTEM_ENABLE_GUESTFS_FUSE_WORKAROUND=1');
let procdir = mntdir.get_child('proc');
if (!procdir.query_exists(cancellable)) {
ProcUtil.runSync(adminCmd.concat(['init-fs', mntdir.get_path()]), cancellable,
{logInitiation: true, env: adminEnv});
}
let revOrTarget;
if (revision)
revOrTarget = revision;
else
revOrTarget = target;
// Remove any existing bootloader configuration, and stub out an
// empty syslinux configuration that we can use to bootstrap.
let bootLoaderLink = mntdir.resolve_relative_path('boot/loader');
GSystem.shutil_rm_rf(bootLoaderLink, cancellable);
let bootLoaderDir0 = mntdir.resolve_relative_path('boot/loader.0');
GSystem.shutil_rm_rf(bootLoaderDir0, cancellable);
bootLoaderLink.make_symbolic_link('loader.0', cancellable);
GSystem.file_ensure_directory(bootLoaderDir0, true, cancellable);
let syslinuxPath = mntdir.resolve_relative_path('boot/loader/syslinux.cfg');
syslinuxPath.replace_contents('TIMEOUT 10\n', null, false, Gio.FileCreateFlags.NONE, cancellable);
// A compatibility symlink for syslinux
let syslinuxDir = mntdir.resolve_relative_path('boot/syslinux');
GSystem.shutil_rm_rf(syslinuxDir, cancellable);
GSystem.file_ensure_directory(syslinuxDir, true, cancellable);
let syslinuxLink = mntdir.resolve_relative_path('boot/syslinux/syslinux.cfg');
syslinuxLink.make_symbolic_link('../loader/syslinux.cfg', cancellable);
// Also blow alway all existing deployments here for the OS; this
// will clean up disks that were using the old ostree model.
GSystem.shutil_rm_rf(ostreeOsdir, cancellable);
let repoPath = ostreedir.get_child('repo');
let repoArg = '--repo=' + repoPath.get_path();
ProcUtil.runSync(adminCmd.concat(['os-init', osname]), cancellable,
{logInitiation: true, env: adminEnv});
if (originRepoUrl)
ProcUtil.runSync(['ostree', repoArg,
'remote', 'add', osname, originRepoUrl, target],
cancellable, { logInitiation: true });
ProcUtil.runSync(['ostree', repoArg,
'pull-local', '--remote=' + osname, srcrepo.get_path(), revOrTarget], cancellable,
{logInitiation: true, env: adminEnv});
let origin = GLib.KeyFile.new();
origin.set_string('origin', 'refspec', osname + ':' + target);
let [originData, len] = origin.to_data();
let tmpOrigin = Gio.File.new_for_path('origin.tmp');
tmpOrigin.replace_contents(originData, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
let rootArg = 'root=LABEL=gnostree-root';
ProcUtil.runSync(adminCmd.concat(['deploy', '--karg=' + rootArg, '--karg=quiet', '--karg=splash',
'--os=' + osname, '--origin-file=' + tmpOrigin.get_path(), revOrTarget]), cancellable,
{logInitiation: true, env: adminEnv});
let defaultFstab = 'LABEL=gnostree-root / ext4 defaults 1 1\n\
LABEL=gnostree-boot /boot ext4 defaults 1 2\n\
LABEL=gnostree-swap swap swap defaults 0 0\n';
let fstabPath = ostreeOsdir.resolve_relative_path('current/etc/fstab');
fstabPath.replace_contents(defaultFstab, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
};
function bootloaderInstall(diskpath, workdir, osname, cancellable) {
let qemuArgs = getDefaultQemuOptions();
let tmpKernelPath = workdir.get_child('kernel.img');
let tmpInitrdPath = workdir.get_child('initrd.img');
let [gfmnt, mntdir] = newReadWriteMount(diskpath, cancellable);
let ostreeArg;
try {
let [kernelPath, initrdPath] = _findCurrentKernel(mntdir, osname, cancellable)
ostreeArg = _findCurrentOstreeBootArg(mntdir, cancellable);
// Copy
kernelPath.copy(tmpKernelPath, 0, cancellable, null, null);
initrdPath.copy(tmpInitrdPath, 0, cancellable, null, null);
} finally {
gfmnt.umount(cancellable);
}
let consoleOutput = workdir.get_child('bootloader-console.out');
let kernelArgv = ['console=ttyS0', 'panic=1', 'root=LABEL=gnostree-root', 'rw', ostreeArg,
'systemd.journald.forward_to_console=true',
'systemd.unit=gnome-ostree-install-bootloader.target'];
qemuArgs.push.apply(qemuArgs, ['-drive', 'file=' + diskpath.get_path() + ',if=virtio',
'-vnc', 'none',
'-no-reboot',
'-serial', 'file:' + consoleOutput.get_path(),
'-chardev', 'socket,id=charmonitor,path=qemu.monitor,server,nowait',
'-mon', 'chardev=charmonitor,id=monitor,mode=control',
'-kernel', tmpKernelPath.get_path(),
'-initrd', tmpInitrdPath.get_path(),
'-append', kernelArgv.join(' ')
]);
ProcUtil.runSync(qemuArgs, cancellable, { cwd: workdir.get_path(),
logInitiation: true });
tmpKernelPath.delete(cancellable);
tmpInitrdPath.delete(cancellable);
}

View File

@ -0,0 +1,93 @@
// 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 Format = imports.format;
const BUILTINS = ['autobuilder',
'git-mirror',
'make',
'qa-make-disk',
'run-task',
'shell'];
function getModule(unixName) {
return imports.builtins[unixName.replace(/-/g, '_')];
}
function getClass(unixName) {
let module = getModule(unixName);
let camelParts = unixName.split(/-/);
let camel = camelParts.map(function (part) {
return part[0].toLocaleUpperCase() + part.substr(1);
}).join('');
return module[camel];
}
function usage(ecode) {
print("Builtins:");
for (let i = 0; i < BUILTINS.length; i++) {
let unixName = BUILTINS[i];
let description = getClass(unixName).prototype.DESCRIPTION;
print(Format.vprintf(" %s - %s", [unixName, description]));
}
return ecode;
}
let ecode;
if (ARGV.length < 1) {
ecode = usage(1);
} else if (ARGV[0] == '-h' || ARGV[0] == '--help') {
ecode = usage(0);
} else {
let name = ARGV[0];
let found = false;
for (let i = 0; i < BUILTINS.length; i++) {
if (BUILTINS[i] == name) {
found = true;
break;
}
}
if (!found) {
usage(1);
} else {
let argv = ARGV.concat();
argv.shift();
let loop = GLib.MainLoop.new(null, true);
let cls = getClass(name);
let instance = new cls;
let cancellable = null;
GLib.idle_add(GLib.PRIORITY_DEFAULT,
function() {
ecode = 1;
try {
instance.main(argv, loop, cancellable);
ecode = 0;
} finally {
loop.quit();
}
return false;
});
loop.run();
}
}
ecode;

View File

@ -0,0 +1,34 @@
// Taken from gnome-shell/js/misc/params.js under the GNU General Public License
// parse:
// @params: caller-provided parameter object, or %null
// @defaults: function-provided defaults object
// @allowExtras: whether or not to allow properties not in @default
//
// Examines @params and fills in default values from @defaults for
// any properties in @defaults that don't appear in @params. If
// @allowExtras is not %true, it will throw an error if @params
// contains any properties that aren't in @defaults.
//
// If @params is %null, this returns the values from @defaults.
//
// Return value: a new object, containing the merged parameters from
// @params and @defaults
function parse(params, defaults, allowExtras) {
let ret = {}, prop;
if (!params)
params = {};
for (prop in params) {
if (!(prop in defaults) && !allowExtras)
throw new Error('Unrecognized parameter "' + prop + '"');
ret[prop] = params[prop];
}
for (prop in defaults) {
if (!(prop in params))
ret[prop] = defaults[prop];
}
return ret;
}

View File

@ -0,0 +1,240 @@
// 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 Format = imports.format;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const GSystem = imports.gi.GSystem;
const Params = imports.params;
const StreamUtil = imports.streamutil;
function objectToEnvironment(o) {
let r = [];
for (let k in o)
r.push(k + "=" + o[k]);
return r;
}
function _newContext(argv, params) {
let context = new GSystem.SubprocessContext({argv: argv});
params = Params.parse(params, {cwd: null,
env: null,
stderr: null,
logInitiation: false });
if (typeof(params.cwd) == 'string')
context.set_cwd(params.cwd);
else if (params.cwd)
context.set_cwd(params.cwd.get_path());
if (params.env)
context.set_environment(params.env);
if (params.stderr != null)
context.set_stderr_disposition(params.stderr);
return [context, params];
}
function _wait_sync_check_internal(proc, cancellable) {
try {
proc.wait_sync_check(cancellable);
} catch (e) {
if (e.domain == GLib.spawn_exit_error_quark() ||
e.matches(GLib.SpawnError, GLib.SpawnError.FAILED)) {
let err = new Error(Format.vprintf("Child process %s: %s", [JSON.stringify(proc.context.argv), e.message]));
err.origError = e;
throw err;
} else {
throw e;
}
}
}
function runSync(argv, cancellable, params) {
let [context, pparams] = _newContext(argv, params);
let proc = new GSystem.Subprocess({context: context});
proc.init(cancellable);
if (pparams.logInitiation)
print(Format.vprintf("Started child process %s: pid=%s", [proc.context.argv.map(GLib.shell_quote).join(' '), proc.get_pid()]));
_wait_sync_check_internal(proc, cancellable);
}
function _runSyncGetOutputInternal(argv, cancellable, params, subParams) {
subParams = Params.parse(subParams, { splitLines: false,
grep: null });
let [context, pparams] = _newContext(argv, params);
context.set_stdout_disposition(GSystem.SubprocessStreamDisposition.PIPE);
context.set_stderr_disposition(GSystem.SubprocessStreamDisposition.INHERIT);
let proc = new GSystem.Subprocess({context: context});
proc.init(cancellable);
if (pparams.logInitiation)
print(Format.vprintf("Started child process %s: pid=%s", [JSON.stringify(proc.context.argv), proc.get_pid()]));
let input = proc.get_stdout_pipe();
let dataIn = Gio.DataInputStream.new(input);
let result = null;
if (subParams.grep) {
let grep = subParams.grep;
while (true) {
let [line, len] = dataIn.read_line_utf8(cancellable);
if (line == null)
break;
result = grep.exec(line);
if (result != null) {
break;
}
}
} else if (subParams.splitLines) {
result = StreamUtil.dataInputStreamReadLines(dataIn, cancellable);
} else {
result = '';
while (true) {
let [line, len] = dataIn.read_line_utf8(cancellable);
if (line == null)
break;
result += (line + '\n');
}
}
dataIn.close(cancellable);
_wait_sync_check_internal(proc, cancellable);
return result;
}
function runSyncGetOutputLines(args, cancellable, params) {
return _runSyncGetOutputInternal(args, cancellable, params, { splitLines: true });
}
function runSyncGetOutputUTF8(args, cancellable, params) {
return _runSyncGetOutputInternal(args, cancellable, params);
}
function runSyncGetOutputUTF8Stripped(args, cancellable, params) {
return _runSyncGetOutputInternal(args, cancellable, params).replace(/[ \n]+$/, '');
}
function runSyncGetOutputUTF8StrippedOrNull(args, cancellable, params) {
if (!params)
params = {};
try {
params.stderr = GSystem.SubprocessStreamDisposition.NULL;
return runSyncGetOutputUTF8Stripped(args, cancellable, params);
} catch (e) {
if (e.origError && e.origError.domain == GLib.spawn_exit_error_quark())
return null;
throw e;
}
}
function runSyncGetOutputGrep(args, pattern, cancellable, params) {
return _runSyncGetOutputInternal(args, cancellable, params, { grep: pattern });
}
function getExitStatusAndString(ecode) {
try {
GLib.spawn_check_exit_status(ecode);
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;
}
}
function asyncWaitCheckFinish(process, result) {
let [waitSuccess, ecode] = process.wait_finish(result);
return getExitStatusAndString(ecode);
}
function runWithTempContextAndLoop(func) {
let mainContext = new GLib.MainContext();
let mainLoop = GLib.MainLoop.new(mainContext, true);
try {
mainContext.push_thread_default();
return func(mainLoop);
} finally {
mainContext.pop_thread_default();
}
}
function _runProcWithInputSyncGetLinesInternal(mainLoop, argv, cancellable, input) {
let context = new GSystem.SubprocessContext({ argv: argv });
context.set_stdout_disposition(GSystem.SubprocessStreamDisposition.PIPE);
context.set_stdin_disposition(GSystem.SubprocessStreamDisposition.PIPE);
let proc = new GSystem.Subprocess({context: context});
proc.init(cancellable);
let stdinPipe = proc.get_stdin_pipe();
let memStream = Gio.MemoryInputStream.new_from_bytes(new GLib.Bytes(input));
let asyncOps = 3;
function asyncOpComplete() {
asyncOps--;
if (asyncOps == 0)
mainLoop.quit();
}
function onSpliceComplete(stdinPipe, result) {
try {
let bytesWritten = stdinPipe.splice_finish(result);
} finally {
asyncOpComplete();
}
}
let closeBoth = Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | Gio.OutputStreamSpliceFlags.CLOSE_TARGET;
stdinPipe.splice_async(memStream, closeBoth, GLib.PRIORITY_DEFAULT,
cancellable, onSpliceComplete);
let procException = null;
function onProcExited(proc, result) {
try {
let [success, statusText] = asyncWaitCheckFinish(proc, result);
if (!success)
procException = statusText;
} finally {
asyncOpComplete();
}
}
proc.wait(cancellable, onProcExited);
let stdoutPipe = proc.get_stdout_pipe();
let stdoutData = Gio.DataInputStream.new(stdoutPipe);
let lines = [];
function onReadLine(datastream, result) {
try {
let [line, len] = stdoutData.read_line_finish_utf8(result);
if (line == null)
asyncOpComplete();
else {
lines.push(line);
stdoutData.read_line_async(GLib.PRIORITY_DEFAULT, cancellable, onReadLine);
}
} catch (e) {
asyncOpComplete();
throw e;
}
}
stdoutData.read_line_async(GLib.PRIORITY_DEFAULT, cancellable, onReadLine);
mainLoop.run();
return lines;
}
function runProcWithInputSyncGetLines(argv, cancellable, input) {
return runWithTempContextAndLoop(function (loop) {
return _runProcWithInputSyncGetLinesInternal(loop, argv, cancellable, input);
});
}

View File

@ -0,0 +1,199 @@
//
// 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.
const Lang = imports.lang;
const JsonUtil = imports.jsonutil;
const Params = imports.params;
function _componentDict(snapshot) {
let r = {};
let components = snapshot['components'];
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'];
if (patches['name'])
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];
}
function fromFile(path, cancellable, args) {
let data = JsonUtil.loadJson(path, cancellable);
return new Snapshot(data, path, args);
}
const Snapshot = new Lang.Class({
Name: 'Snapshot',
_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) {
if (!componentMeta)
return {};
let result = {};
Lang.copyProperties(componentMeta, result);
let origSrc = componentMeta['src'];
let name = componentMeta['name'];
if (origSrc.indexOf('tarball:') == 0) {
if (!name)
throw new Error("Component src " + origSrc + " has no name attribute");
if (!componentMeta['checksum'])
throw new Error("Component src " + origSrc + " has no checksum attribute");
return result;
}
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 src, idx;
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);
let patchMeta = this.data['patches'];
if (patchMeta) {
let componentPatchFiles = component['patches'] || [];
if (componentPatchFiles.length > 0) {
let patches = {};
Lang.copyProperties(patchMeta, patches);
patches['files'] = componentPatchFiles;
r['patches'] = patches;
}
}
let configOpts = (this.data['config-opts'] || []).concat();
configOpts.push.apply(configOpts, component['config-opts'] || []);
r['config-opts'] = configOpts;
return r;
},
getAllComponentNames: function() {
return this._componentNames;
},
getComponentMap: function() {
return this._componentDict;
},
getComponent: function(name, allowNone) {
let r = this._componentDict[name] || null;
if (!r && !allowNone)
throw new Error("No component " + name + " in snapshot");
return r;
},
getMatchingSrc: function(src, allowNone) {
let result = [];
for (let i = 0; i < this._componentNames.length; i++) {
let name = this._componentNames[i];
let component = this.getComponent(name, false);
if (component['src'] == src)
result.push(component);
}
return result;
},
getExpanded: function(name) {
return this._expandComponent(this.getComponent(name));
}
});

View File

@ -0,0 +1,27 @@
// 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.
function dataInputStreamReadLines(dataIn, cancellable) {
let result = [];
while (true) {
let [line, len] = dataIn.read_line_utf8(cancellable);
if (line == null)
break;
result.push(line);
}
return result;
}

469
src/autobuilder/js/task.js Normal file
View File

@ -0,0 +1,469 @@
// 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 OSTree = imports.gi.OSTree;
const Params = imports.params;
const VersionedDir = imports.versioneddir;
const JsonUtil = imports.jsonutil;
const ProcUtil = imports.procutil;
const BuildUtil = imports.buildutil;
const DefaultTaskDef = {
TaskName: '',
TaskAfter: [],
TaskScheduleMinSecs: 0,
PreserveStdout: true,
RetainFailed: 5,
RetainSuccess: 5,
};
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);
}
}
denum.close(null);
},
register: function(task) {
this._tasks.push(task);
},
getAllTasks: function() {
return this._tasks;
},
getTask: function(taskName) {
for (let i = 0; i < this._tasks.length; i++) {
let taskConstructor = this._tasks[i];
let taskDef = taskConstructor.prototype.TaskDef;
let curName = taskDef.TaskName;
if (curName == taskName)
return taskConstructor;
}
throw new Error("No task definition matches " + taskName);
},
getTaskDef: function(taskName) {
let taskDef = this.getTask(taskName).prototype.TaskDef;
return Params.parse(taskDef, DefaultTaskDef);
},
getInstance: function() {
if (!_tasksetInstance)
_tasksetInstance = new TaskSet();
return _tasksetInstance;
},
getTasksAfter: function(taskName) {
let ret = [];
for (let i = 0; i < this._tasks.length; i++) {
let taskConstructor = this._tasks[i];
let taskDef = Params.parse(taskConstructor.prototype.TaskDef, DefaultTaskDef);
let after = taskDef.TaskAfter;
for (let j = 0; j < after.length; j++) {
let a = after[j];
if (a == taskName) {
ret.push(taskDef.TaskName);
break;
}
}
}
return ret;
}
});
const TaskData = new Lang.Class({
Name: 'TaskData',
_init: function(taskDef, parameters) {
this.name = taskDef.TaskName;
this.taskDef = taskDef;
this.parameters = parameters;
},
});
const TaskMaster = new Lang.Class({
Name: 'TaskMaster',
_init: function(workdir, params) {
params = Params.parse(params, { onEmpty: null,
processAfter: true,
skip: [] });
this.workdir = workdir;
this.tasksPath = workdir.get_child('tasks');
GSystem.file_ensure_directory(this.tasksPath, true, null);
this.completedTasksPath = workdir.get_child('results/tasks');
GSystem.file_ensure_directory(this.completedTasksPath, true, null);
this._processAfter = params.processAfter;
this._skipTasks = {};
for (let i = 0; i < params.skip.length; i++)
this._skipTasks[params.skip[i]] = true;
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();
// string -> [ lastExecutedSecs, taskData ]
this._scheduledTaskTimeouts = {};
},
_setTaskBuildPath: function(taskName, buildPath) {
let taskLink = this.tasksPath.get_child(taskName);
BuildUtil.atomicSymlinkSwap(taskLink, buildPath, this.cancellable);
},
_pushTaskDataImmediate: function(taskData) {
this._pendingTasksList.push(taskData);
this._queueRecalculate();
},
pushTask: function(buildPath, taskName, parameters) {
this._setTaskBuildPath(taskName, buildPath);
this._pushTask(taskName, parameters);
},
_pushTask: function(name, parameters) {
let taskDef = this._taskset.getTaskDef(name);
let taskData = new TaskData(taskDef, parameters);
if (!this._isTaskPending(name)) {
let scheduleMinSecs = taskDef.TaskScheduleMinSecs;
if (scheduleMinSecs > 0) {
let info = this._scheduledTaskTimeouts[name];
if (!info) {
info = [ 0, null ];
this._scheduledTaskTimeouts[name] = info;
}
let lastExecutedSecs = info[0];
let pendingExecData = info[1];
let currentTime = GLib.get_monotonic_time() / GLib.USEC_PER_SEC;
if (pendingExecData != null) {
// Nothing, already scheduled
let delta = (lastExecutedSecs + scheduleMinSecs) - currentTime;
print("Already scheduled task " + name + " remaining=" + delta);
} else if (lastExecutedSecs == 0) {
print("Scheduled task " + name + " executing immediately");
this._pushTaskDataImmediate(taskData);
info[0] = currentTime;
} else {
let delta = (lastExecutedSecs + scheduleMinSecs) - currentTime;
print("Scheduled task " + name + " delta=" + delta);
let timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
Math.max(delta, 0),
Lang.bind(this, this._executeScheduledTask, name));
info[0] = currentTime;
info[1] = taskData;
}
} else {
this._pushTaskDataImmediate(taskData);
}
}
},
_executeScheduledTask: function(name) {
print("Executing scheduled task " + name);
let currentTime = GLib.get_monotonic_time() / GLib.USEC_PER_SEC;
let info = this._scheduledTaskTimeouts[name];
let taskData = info[1];
info[0] = currentTime;
info[1] = null;
this._pushTaskDataImmediate(taskData);
},
_isTaskPending: function(taskName) {
for (let i = 0; i < this._pendingTasksList.length; i++) {
let pending = this._pendingTasksList[i];
if (pending.name == taskName)
return true;
}
return false;
},
isTaskQueued: function(taskName) {
return this._isTaskPending(taskName) || this.isTaskExecuting(taskName);
},
isTaskExecuting: function(taskName) {
for (let i = 0; i < this._executing.length; i++) {
let executingRunner = this._executing[i];
if (executingRunner.taskData.name == 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) {
if (this._onEmpty)
this._onEmpty();
return;
} else if (this._pendingTasksList.length == 0) {
return;
}
let notExecuting = [];
let executing = [];
for (let i = 0; i < this._pendingTasksList.length; i++) {
let pending = this._pendingTasksList[i];
if (this.isTaskExecuting(pending.name))
executing.push(pending);
else
notExecuting.push(pending);
}
this._pendingTasksList = notExecuting.concat(executing);
this._reschedule();
},
_onComplete: function(success, error, runner) {
let taskName = runner.taskData.name;
let idx = -1;
for (let i = 0; i < this._executing.length; i++) {
let executingRunner = this._executing[i];
if (executingRunner !== runner)
continue;
idx = i;
break;
}
if (idx == -1)
throw new Error("TaskMaster: Internal error - Failed to find completed task:" + taskName);
this._executing.splice(idx, 1);
let link = this.completedTasksPath.get_child(taskName);
BuildUtil.atomicSymlinkSwap(link, runner.buildPath, this.cancellable);
if (success && runner.changed) {
let taskDef = runner.taskData.taskDef;
let after = this._taskset.getTasksAfter(taskName);
for (let i = 0; i < after.length; i++) {
let afterTaskName = after[i];
this._setTaskBuildPath(afterTaskName, runner.buildPath);
if (!this._skipTasks[afterTaskName] && this._processAfter)
this._pushTask(afterTaskName, {});
}
}
this.emit('task-complete', runner, success, error);
this._queueRecalculate();
},
_reschedule: function() {
while (this._executing.length < this.maxConcurrent &&
this._pendingTasksList.length > 0 &&
!this.isTaskExecuting(this._pendingTasksList[0].name)) {
let task = this._pendingTasksList.shift();
let runner = new TaskRunner(this, task, Lang.bind(this, function(success, error) {
this._onComplete(success, error, runner);
}));
runner.executeInSubprocess(this.cancellable);
this._executing.push(runner);
this.emit('task-executing', runner);
}
}
});
Signals.addSignalMethods(TaskMaster.prototype);
const Task = new Lang.Class({
Name: 'Task',
DefaultParameters: {},
_init: function(parameters) {
this.name = this.TaskName;
this.parameters = Params.parse(parameters, this.DefaultParameters);
this.workdir = Gio.File.new_for_path(GLib.getenv('_OSTBUILD_WORKDIR'));
BuildUtil.checkIsWorkDirectory(this.workdir);
this.builddir = Gio.File.new_for_path(GLib.getenv('_OSTBUILD_BUILDDIR'));
let relpath = this.workdir.get_relative_path(this.builddir);
if (relpath == 'local') {
this._buildName = 'local';
} else if (relpath.indexOf('builds/') == 0) {
relpath = relpath.substring(7);
this._buildName = VersionedDir.relpathToVersion(relpath);
} else {
throw new Error();
}
this.mirrordir = this.workdir.get_child('src');
GSystem.file_ensure_directory(this.mirrordir, true, null);
this.cachedir = this.workdir.resolve_relative_path('cache/raw');
GSystem.file_ensure_directory(this.cachedir, true, null);
this.libdir = Gio.File.new_for_path(GLib.getenv('OSTBUILD_LIBDIR'));
this.repo = this.workdir.get_child('repo');
this.ostreeRepo = new OSTree.Repo({ path: this.repo });
if (!this.ostreeRepo.get_path().query_exists(null))
this.ostreeRepo.create(OSTree.RepoMode.ARCHIVE_Z2, null);
this.ostreeRepo.open(null);
},
execute: function(cancellable) {
throw new Error("Not implemented");
},
});
const TaskRunner = new Lang.Class({
Name: 'TaskRunner',
_init: function(taskmaster, taskData, onComplete) {
this.taskmaster = taskmaster;
this.taskData = taskData;
this.onComplete = onComplete;
this.name = taskData.name;
this.workdir = taskmaster.workdir;
BuildUtil.checkIsWorkDirectory(this.workdir);
},
executeInSubprocess: function(cancellable) {
this._cancellable = cancellable;
this._startTimeMillis = GLib.get_monotonic_time() / 1000;
// To prevent tasks from stomping on each other's toes, we put the task
// cwd in its own task dir. If a task has any results it wants to pass
// on between builds, it needs to write to _OSTBUILD_BUILDDIR.
let buildPath = this.taskmaster.tasksPath.resolve_relative_path(this.name);
this.buildPath = GSystem.file_realpath(buildPath);
this.buildName = this.workdir.get_relative_path(this.buildPath);
this.taskCwd = this.buildPath.get_child(this.name);
GSystem.file_ensure_directory(this.taskCwd, false, cancellable);
let baseArgv = ['ostbuild', 'run-task', this.name, JSON.stringify(this.taskData.parameters)];
let context = new GSystem.SubprocessContext({ argv: baseArgv });
context.set_cwd(this.taskCwd.get_path());
let childEnv = GLib.get_environ();
childEnv.push('_OSTBUILD_BUILDDIR=' + this.buildPath.get_path());
childEnv.push('_OSTBUILD_WORKDIR=' + this.workdir.get_path());
context.set_environment(childEnv);
if (this.taskData.taskDef.PreserveStdout) {
let outPath = this.taskCwd.get_child('output.txt');
GSystem.shutil_rm_rf(outPath, cancellable);
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.taskCwd.get_child('errors.txt');
context.set_stderr_file_path(errPath.get_path());
}
this._proc = new GSystem.Subprocess({ context: context });
this._proc.init(cancellable);
let targetPath = this.workdir.get_relative_path(this.taskCwd);
let meta = { taskMetaVersion: 0,
buildPath: this.buildName,
complete: false,
path: targetPath };
JsonUtil.writeJsonFileAtomic(this.taskCwd.get_child('meta.json'), meta, cancellable);
this._proc.wait(cancellable, Lang.bind(this, this._onChildExited));
},
_onChildExited: function(proc, result) {
let cancellable = this._cancellable;
let [success, errmsg] = ProcUtil.asyncWaitCheckFinish(proc, result);
let target;
this.changed = true;
let modifiedPath = this.taskCwd.get_child('modified.json');
if (modifiedPath.query_exists(null)) {
let data = JsonUtil.loadJson(modifiedPath, null);
this.changed = data['modified'];
}
this.onComplete(success, errmsg);
if (!this.changed)
return;
let elapsedMillis = GLib.get_monotonic_time() / 1000 - this._startTimeMillis;
let targetPath = this.workdir.get_relative_path(this.taskCwd);
let meta = { taskMetaVersion: 0,
buildPath: this.buildName,
complete: true,
success: success,
errmsg: errmsg,
elapsedMillis: elapsedMillis,
path: targetPath };
let statusTxtPath = this.taskCwd.get_child('status.txt');
if (statusTxtPath.query_exists(null)) {
let contents = GSystem.file_load_contents_utf8(statusTxtPath, cancellable);
meta['status'] = contents.replace(/[ \n]+$/, '');
}
JsonUtil.writeJsonFileAtomic(this.taskCwd.get_child('meta.json'), meta, cancellable);
}
});

View File

@ -0,0 +1,140 @@
// 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 ProcUtil = imports.procutil;
const StreamUtil = imports.streamutil;
const JsonUtil = imports.jsonutil;
const Snapshot = imports.snapshot;
const BuildUtil = imports.buildutil;
const Vcs = imports.vcs;
const ArgParse = imports.argparse;
const TaskBdiff = new Lang.Class({
Name: "TaskBdiff",
Extends: Task.Task,
TaskDef: {
TaskName: "bdiff",
TaskAfter: ['build'],
},
_gitLogToJson: function(repoDir, specification) {
let log = ProcUtil.runSyncGetOutputLines(['git', 'log', '--format=email', specification],
null,
{ cwd: repoDir, logInitiation: true });
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;
}
}
}
if (currentItem !== null) {
r.push(currentItem);
}
return r;
},
_diffstat: function(repoDir, specification) {
return ProcUtil.runSyncGetOutputUTF8(['git', 'diff', '--stat', specification], null,
{ cwd: repoDir });
},
execute: function(cancellable) {
let latestSnapshotPath = this.builddir.get_child('snapshot.json');
let previousSnapshotPath = this.builddir.get_child('last-build/snapshot.json');
if (!previousSnapshotPath.query_exists(cancellable))
return;
let latestBuildSnapshot = Snapshot.fromFile(latestSnapshotPath, cancellable);
let previousBuildSnapshot = Snapshot.fromFile(previousSnapshotPath, cancellable);
let added = [];
let modified = [];
let removed = [];
let result = { 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 mirrordir = Vcs.ensureVcsMirror(this.mirrordir, previousComponent, cancellable);
let gitlog = this._gitLogToJson(mirrordir, previousRevision + '...' + latestRevision);
let diffstat = this._diffstat(mirrordir, previousRevision + '..' + latestRevision);
modified.push({ previous: previousComponent,
latest: latestComponent,
gitlog: gitlog,
diffstat: diffstat });
}
JsonUtil.writeJsonFileAtomic(this.builddir.get_child('bdiff.json'), result, cancellable);
}
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,153 @@
// -*- 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 JsonUtil = imports.jsonutil;
const JSUtil = imports.jsutil;
const GuestFish = imports.guestfish;
const IMAGE_RETAIN_COUNT = 2;
const TaskBuildDisks = new Lang.Class({
Name: 'TaskBuildDisks',
Extends: Task.Task,
TaskDef: {
TaskName: "builddisks",
TaskAfter: ['build'],
},
_imageSubdir: 'images',
_inheritPreviousDisk: true,
_onlyTreeSuffixes: ['-runtime', '-devel-debug'],
execute: function(cancellable) {
let isLocal = this._buildName == 'local';
let baseImageDir = this.workdir.resolve_relative_path(this._imageSubdir);
GSystem.file_ensure_directory(baseImageDir, true, cancellable);
let currentImageLink = baseImageDir.get_child('current');
let targetImageDir = baseImageDir.get_child(this._buildName);
if (!isLocal && targetImageDir.query_exists(null)) {
print("Already created " + targetImageDir.get_path());
return;
}
print("Creating image for buildName=" + this._buildName);
let buildDataPath = this.builddir.get_child('build.json');
let buildData = JsonUtil.loadJson(buildDataPath, cancellable);
let workImageDir = Gio.File.new_for_path('images');
GSystem.file_ensure_directory(workImageDir, true, cancellable);
let destPath = workImageDir.get_child('build-' + this._buildName + '.json');
GSystem.shutil_rm_rf(destPath, cancellable);
GSystem.file_linkcopy(buildDataPath, destPath, Gio.FileCopyFlags.ALL_METADATA, cancellable);
let targets = buildData['targets'];
let osname = buildData['snapshot']['osname'];
let originRepoUrl = buildData['snapshot']['repo'];
for (let targetName in targets) {
let matched = false;
for (let i = 0; i < this._onlyTreeSuffixes.length; i++) {
if (JSUtil.stringEndswith(targetName, this._onlyTreeSuffixes[i])) {
matched = true;
break;
}
}
if (!matched)
continue;
let targetRevision = buildData['targets'][targetName];
let squashedName = osname + '-' + targetName.substr(targetName.lastIndexOf('/') + 1);
let diskName = squashedName + '.qcow2';
let diskPath = workImageDir.get_child(diskName);
let prevPath = currentImageLink.get_child(diskName);
let prevExists = prevPath.query_exists(null);
GSystem.shutil_rm_rf(diskPath, cancellable);
let doCloneDisk = this._inheritPreviousDisk && prevExists;
if (doCloneDisk) {
LibQA.copyDisk(prevPath, diskPath, cancellable);
} else {
LibQA.createDisk(diskPath, cancellable);
}
let mntdir = Gio.File.new_for_path('mnt-' + squashedName);
GSystem.file_ensure_directory(mntdir, true, cancellable);
let gfmnt = new GuestFish.GuestMount(diskPath, { partitionOpts: LibQA.DEFAULT_GF_PARTITION_OPTS,
readWrite: true });
gfmnt.mount(mntdir, cancellable);
try {
LibQA.pullDeploy(mntdir, this.repo, osname, targetName, targetRevision, originRepoUrl,
cancellable);
} finally {
gfmnt.umount(cancellable);
}
// Assume previous disks have successfully installed a bootloader
if (!doCloneDisk) {
LibQA.bootloaderInstall(diskPath, Gio.File.new_for_path('.'), osname, cancellable);
print("Bootloader installation complete");
}
this._postDiskCreation(squashedName, diskPath, cancellable);
print("post-disk creation complete");
}
if (isLocal) {
let localImageDir = baseImageDir.get_child('local');
GSystem.shutil_rm_rf(localImageDir, cancellable);
GSystem.file_rename(workImageDir, localImageDir, cancellable);
return;
}
GSystem.file_rename(workImageDir, targetImageDir, cancellable);
let currentInfo = null;
let oldCurrent = 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) {
oldCurrent = currentImageLink.get_parent().resolve_relative_path(currentInfo.get_symlink_target());
}
BuildUtil.atomicSymlinkSwap(baseImageDir.get_child('current'), targetImageDir, cancellable);
if (!isLocal && oldCurrent)
GSystem.shutil_rm_rf(oldCurrent, cancellable);
},
_postDiskCreation: function(squashedName, diskPath, cancellable) {
// Nothing, this is used by zdisks
}
});

View File

@ -0,0 +1,99 @@
// 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 Gio = imports.gi.Gio;
const Lang = imports.lang;
const GSystem = imports.gi.GSystem;
const Task = imports.task;
const ProcUtil = imports.procutil;
const JsonUtil = imports.jsonutil;
const Snapshot = imports.snapshot;
const Vcs = imports.vcs;
const TaskResolve = new Lang.Class({
Name: "TaskResolve",
Extends: Task.Task,
TaskDef: {
TaskName: "resolve",
},
DefaultParameters: {fetchAll: false,
fetchSrcUrls: [],
fetchComponents: [],
timeoutSec: 10},
_writeSnapshotToBuild: function(cancellable) {
let data = this._snapshot.data;
let buf = JsonUtil.serializeJson(data);
let oldSnapshot = this.builddir.get_child('last-build/snapshot.json');
if (oldSnapshot.query_exists(cancellable)) {
let oldBytes = GSystem.file_map_readonly(oldSnapshot, cancellable);
let oldCsum = GLib.compute_checksum_for_bytes(GLib.ChecksumType.SHA256, oldBytes);
let newCsum = GLib.compute_checksum_for_string(GLib.ChecksumType.SHA256, buf, -1);
if (oldCsum == newCsum)
return false;
}
let snapshot = this.builddir.get_child('snapshot.json');
JsonUtil.writeJsonFileAtomic(snapshot, data, cancellable);
return true;
},
execute: function(cancellable) {
let manifestPath = this.workdir.get_child('manifest.json');
this._snapshot = Snapshot.fromFile(manifestPath, cancellable, { prepareResolve: true });
let componentsToFetch = this.parameters.fetchComponents.slice();
let srcUrls = this.parameters.fetchSrcUrls;
for (let i = 0; i < srcUrls; i++) {
let matches = snapshot.getMatchingSrc(srcUrls[i]);
componentsToFetch.push.apply(matches);
}
let gitMirrorArgs = ['ostbuild', 'git-mirror', '--timeout-sec=' + this.parameters.timeoutSec,
'--workdir=' + this.workdir.get_path(),
'--manifest=' + manifestPath.get_path()];
if (this.parameters.fetchAll || componentsToFetch.length > 0) {
gitMirrorArgs.push('--fetch');
gitMirrorArgs.push('-k');
gitMirrorArgs.push.apply(gitMirrorArgs, componentsToFetch);
}
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 tagOrBranch = component['tag'] || component['branch'];
let mirrordir = Vcs.ensureVcsMirror(this.mirrordir, component, cancellable);
let revision = Vcs.describeVersion(mirrordir, tagOrBranch);
component['revision'] = revision;
}
let modified = this._writeSnapshotToBuild(cancellable);
if (modified) {
print("New source snapshot");
} else {
print("Source snapshot unchanged");
}
let modifiedPath = Gio.File.new_for_path('modified.json');
JsonUtil.writeJsonFileAtomic(modifiedPath, { modified: modified }, cancellable);
}
});

View File

@ -0,0 +1,52 @@
// -*- 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 ProcUtil = imports.procutil;
const Task = imports.task;
const TestBase = imports.tasks.testbase;
const LibQA = imports.libqa;
const JSUtil = imports.jsutil;
const JSONUtil = imports.jsonutil;
const TaskSmoketest = new Lang.Class({
Name: 'TaskSmoketest',
Extends: TestBase.TestBase,
TaskDef: {
TaskName: "smoketest",
TaskAfter: ['builddisks'],
},
RequiredMessageIDs: ["0ce153587afa4095832d233c17a88001" // gnome-session startup ok
],
FailedMessageIDs: ["fc2e22bc6ee647b6b90729ab34a250b1", // coredump
"10dd2dc188b54a5e98970f56499d1f73" // gnome-session required component failed
],
CompletedTag: 'smoketested'
});

View File

@ -0,0 +1,49 @@
// -*- 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 Gio = imports.gi.Gio;
const Lang = imports.lang;
const BuildDisks = imports.tasks['task-builddisks'];
const TaskZDisks = new Lang.Class({
Name: 'TaskZDisks',
Extends: BuildDisks.TaskBuildDisks,
TaskDef: {
TaskName: "zdisks",
TaskAfter: ['smoketest'],
TaskScheduleMinSecs: 3*60*60, // Only do this every 3 hours
},
_imageSubdir: 'images/z',
_inheritPreviousDisk: false,
_onlyTreeSuffixes: ['-runtime', '-devel-debug'],
_postDiskCreation: function(squashedName, diskPath, cancellable) {
let parent = diskPath.get_parent();
let outPath = parent.get_child(squashedName + '-' + this._buildName + '.qcow2.gz');
let outStream = outPath.create(Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
let compressor = Gio.ZlibCompressor.new(Gio.ZlibCompressorFormat.GZIP, 7);
let outConverter = Gio.ConverterOutputStream.new(outStream, compressor);
let inStream = diskPath.read(cancellable);
outConverter.splice(inStream, Gio.OutputStreamSpliceFlags.CLOSE_SOURCE |
Gio.OutputStreamSpliceFlags.CLOSE_TARGET, cancellable);
diskPath.delete(cancellable);
}
});

View File

@ -0,0 +1,646 @@
// -*- 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 ProcUtil = imports.procutil;
const Params = imports.params;
const Task = imports.task;
const LibQA = imports.libqa;
const JSUtil = imports.jsutil;
const JSONUtil = imports.jsonutil;
const TIMEOUT_SECONDS = 10 * 60;
const CommandSocketIface = '<node> \
<interface name="org.gnome.Continuous.Command"> \
<method name="AsyncMessage"> \
<arg name="msgId" direction="in" type="s"/> \
<arg name="value" direction="in" type="v"/> \
</method> \
<method name="Screenshot"> \
<arg name="name" direction="in" type="s"/> \
</method> \
<signal name="ScreenshotComplete"> \
<arg name="name" direction="out" type="s"/> \
</signal> \
</interface> \
</node>';
const CommandSocketProxy = new Lang.Class({
Name: 'CommandSocketProxy',
_init: function(connection,
asyncMessageHandler,
screenshotHandler) {
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(CommandSocketIface, this);
this._dbusImpl.export(connection, '/org/gnome/Continuous/Command');
this._asyncMessageHandler = asyncMessageHandler;
this._screenshotHandler = screenshotHandler;
},
Screenshot: function(name) {
this._screenshotHandler(name);
},
AsyncMessage: function(msgId, value) {
this._asyncMessageHandler(msgId, value);
}
});
const TestOneDisk = new Lang.Class({
Name: 'TestOneDisk',
_init: function(parentTask, testRequiredMessageIds, testFailedMessageIds, testStatusMessageId) {
this._parentTask = parentTask;
this._testRequiredMessageIds = testRequiredMessageIds;
this._testFailedMessageIds = testFailedMessageIds;
this._statusMessageId = testStatusMessageId;
},
_fail: function(message) {
if (this._failed)
return;
this._failed = true;
this._failedMessage = message;
this._screenshot({ isFinal: true });
},
_onQemuExited: function(proc, result) {
let [success, status] = ProcUtil.asyncWaitCheckFinish(proc, result);
this._qemu = null;
this._loop.quit();
if (!success) {
this._fail("Qemu exited with status " + status);
}
},
_onTimeout: function() {
print("Timeout reached");
for (let msgid in this._pendingRequiredMessageIds) {
print("Did not see MESSAGE_ID=" + msgid);
}
this._fail("Timed out");
this._loop.quit();
},
_onJournalOpen: function(file, result) {
try {
this._journalStream = file.read_finish(result);
this._journalDataStream = Gio.DataInputStream.new(this._journalStream);
this._openedJournal = true;
this._readingJournal = true;
this._journalDataStream.read_line_async(GLib.PRIORITY_DEFAULT, this._cancellable,
Lang.bind(this, this._onJournalReadLine));
} catch (e) {
this._fail("Journal open failed: " + e);
this._loop.quit();
}
},
_onJournalReadLine: function(stream, result) {
this._readingJournal = false;
let line, len;
try {
[line, len] = stream.read_line_finish_utf8(result);
} catch (e) {
this._fail(e.toString());
this._loop.quit();
throw e;
}
if (this._foundAllMessageIds || this._failed)
return;
if (!line)
return;
let data = JSON.parse(line);
let message = data['MESSAGE'];
let messageId = data['MESSAGE_ID'];
let identifier = data['SYSLOG_IDENTIFIER'] || data['_EXE'];
if (message)
this._journalTextStream.write_all(identifier + ': ' + message + "\n", null);
if (line) {
if (messageId) {
if (this._pendingRequiredMessageIds[messageId]) {
print("Found required message ID " + messageId);
print(message);
delete this._pendingRequiredMessageIds[messageId];
this._countPendingRequiredMessageIds--;
} else if (this._failMessageIds[messageId]) {
this._fail("Found failure message ID " + messageId);
print(message);
this._loop.quit();
}
if (messageId === this._statusMessageId) {
print(message);
let statusTxtPath = Gio.File.new_for_path('status.txt');
statusTxtPath.replace_contents(message + '\n', null, false,
Gio.FileCreateFlags.REPLACE_DESTINATION, this._cancellable);
}
this._parentTask._handleMessage(data, this._cancellable);
}
if (this._countPendingRequiredMessageIds == 0 && !this._foundAllMessageIds) {
print("Found all required message IDs, waiting for " + this._parentTask.CompleteIdleWaitSeconds);
this._foundAllMessageIds = true;
this._parentTask._onSuccess();
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, this._parentTask.CompleteIdleWaitSeconds,
Lang.bind(this, this._onFinalWait));
} else {
this._readingJournal = true;
this._journalDataStream.read_line_async(GLib.PRIORITY_DEFAULT, this._cancellable,
Lang.bind(this, this._onJournalReadLine));
}
}
},
_onJournalChanged: function(monitor, file, otherFile, eventType) {
if (this._foundAllMessageIds || this._failed)
return;
if (!this._openedJournal) {
this._openedJournal = true;
file.read_async(GLib.PRIORITY_DEFAULT,
this._cancellable,
Lang.bind(this, this._onJournalOpen));
} else if (!this._readingJournal) {
this._readingJournal = true;
this._journalDataStream.read_line_async(GLib.PRIORITY_DEFAULT, this._cancellable,
Lang.bind(this, this._onJournalReadLine));
}
},
_onQemuCapabiltiesRead: function(datain, result) {
print("QMP server greeting received");
let [response, len] = datain.read_line_finish_utf8(result);
this._qmpGreetingReceived = true;
this._qmpCommand({ "execute": "qmp_capabilities" },
Lang.bind(this, function() {
print("qmp_capabilities complete");
this._qmpCapabilitiesReceived = true;
}));
},
_ensureQemuConnection: function() {
if (this._qmpSocketConn)
return false;
let monitorPath = this._subworkdir.get_child("qemu.monitor");
if (!monitorPath.query_exists (null)) {
this._qmpConnectionAttempts++;
if (this._qmpConnectionAttempts > 10)
throw new Error("Failed to connect to qemu monitor after " + this._qmpConnectionAttempts + " attempts");
return true;
}
let path = Gio.File.new_for_path('.').get_relative_path(monitorPath);
let address = Gio.UnixSocketAddress.new_with_type(path, Gio.UnixSocketAddressType.PATH);
let socketClient = new Gio.SocketClient();
this._qmpSocketConn = socketClient.connect(address, this._cancellable);
this._qmpOut = Gio.DataOutputStream.new(this._qmpSocketConn.get_output_stream());
this._qmpIn = Gio.DataInputStream.new(this._qmpSocketConn.get_input_stream());
this._qmpIn.read_line_async(GLib.PRIORITY_DEFAULT, this._cancellable,
Lang.bind(this, this._onQemuCapabiltiesRead));
return false;
},
_onQemuCommandComplete: function(datain, result) {
let [response, len] = datain.read_line_finish_utf8(result);
let responseData = null;
try {
responseData = JSON.parse(response);
} catch (e) {}
if (responseData && responseData.error)
print("command response error=" + JSON.stringify(responseData.error));
let onComplete = this._qmpCommandOutstanding.shift();
if (this._qmpCommandOutstanding.length == 1)
this._qmpIn.read_line_async(GLib.PRIORITY_DEFAULT, this._cancellable,
Lang.bind(this, this._onQemuCommandComplete));
onComplete();
},
_qmpCommand: function(cmd, onComplete) {
this._ensureQemuConnection();
let cmdStr = JSON.stringify(cmd);
if (!this._qmpGreetingReceived)
throw new Error("Attempting QMP command without having received greeting");
this._qmpOut.put_string(cmdStr, this._cancellable);
this._qmpCommandOutstanding.push(onComplete);
if (this._qmpCommandOutstanding.length == 1)
this._qmpIn.read_line_async(GLib.PRIORITY_DEFAULT, this._cancellable,
Lang.bind(this, this._onQemuCommandComplete));
},
_onScreenshotComplete: function(filename, isFinal, name) {
print("screenshot complete for " + filename);
let filePath = this._subworkdir.get_child(filename);
let modified = true;
if (!isFinal && name == null) {
let contentsBytes = GSystem.file_map_readonly(filePath, this._cancellable);
let csum = GLib.compute_checksum_for_bytes(GLib.ChecksumType.SHA256,
contentsBytes);
modified = this._lastScreenshotChecksum != csum;
if (!modified) {
GSystem.file_unlink(filePath, this._cancellable);
} else {
this._lastScreenshotChecksum = csum;
}
this._screenshotSerial++;
}
// Convert to PNG if possible
let screenshotPath = filePath;
if (modified && imports.gi.GdkPixbuf) {
let GdkPixbuf = imports.gi.GdkPixbuf;
let pixbuf = null;
try {
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filePath.get_path());
} catch (e) {
if (e.domain != GdkPixbuf.PixbufError)
throw e;
print(e);
}
if (pixbuf != null) {
let outFilename = this._subworkdir.get_child(filename.replace(/ppm$/, 'png'));
screenshotPath = outFilename;
pixbuf.savev(outFilename.get_path(), "png", [], []);
}
GSystem.file_unlink(filePath, this._cancellable);
}
if (name == null) {
this._requestingScreenshot = false;
} else {
this._parentTask._screenshotTaken(screenshotPath);
this._commandProxy._dbusImpl.emit_signal('ScreenshotComplete',
GLib.Variant.new('(s)', [name]));
}
if (isFinal) {
print("Final screenshot complete");
this._qmpCommand({"execute": "system_powerdown"},
Lang.bind(this, this._onFinalPoweroff));
}
},
_onFinalPoweroff: function() {
print("Poweroff request sent");
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 5,
Lang.bind(this, function() {
print("Poweroff timeout ");
this._loop.quit();
}));
},
_screenshot: function(params) {
params = Params.parse(params, { isFinal: false,
name: null });
if (params.name == null) {
if (this._requestingScreenshot)
return;
this._requestingScreenshot = true;
}
let filename;
if (params.name == null) {
if (params.isFinal)
filename = "screenshot-final.ppm";
else
filename = "screenshot-" + this._screenshotSerial + ".ppm";
} else {
filename = "screenshot-" + params.name + ".ppm";
}
this._qmpCommand({"execute": "screendump", "arguments": { "filename": filename }},
Lang.bind(this, this._onScreenshotComplete, filename, params.isFinal, params.name));
},
_idleScreenshot: function() {
if (this._foundAllMessageIds)
return false;
if (this._qmpCapabilitiesReceived)
this._screenshot(false);
return true;
},
_onFinalWait: function() {
print("Final wait complete at " + GLib.DateTime.new_now_local().format('%c'));
this._screenshot({ isFinal: true });
},
_onCommandChannelScreenshot: function(name) {
this._screenshot({ name: name });
},
_onCommandSocketDBusReady: function(iostream, result) {
this._commandSocketDBus = Gio.DBusConnection.new_finish(result);
this._commandProxy = new CommandSocketProxy(this._commandSocketDBus,
Lang.bind(this._parentTask, this._parentTask._onCommandChannelAsyncMessage),
Lang.bind(this, this._onCommandChannelScreenshot));
print("Command DBus connection open");
},
_onCommandSocketConnected: function(client, result) {
this._commandSocketConn = client.connect_finish(result);
print("Connected to command socket");
Gio.DBusConnection.new(this._commandSocketConn, null, 0,
null, this._cancellable,
Lang.bind(this, this._onCommandSocketDBusReady));
},
_tryCommandConnection: function() {
if (this._commandSocketDBus || this._complete)
return false;
printerr("DEBUG: Querying command socket");
if (!this._commandSocketPath.query_exists(null)) {
print("commandSocketPath " + this._commandSocketPath.get_path() + " does not exist yet");
this._commandConnectionAttempts++;
if (this._commandConnectionAttempts > 5) {
this._fail("Command connection didn't appear at " + this._commandSocketPath.get_path());
this._loop.quit();
return false;
}
return true;
}
printerr("Connecting to command socket...");
let path = Gio.File.new_for_path('.').get_relative_path(this._commandSocketPath);
let address = Gio.UnixSocketAddress.new_with_type(path, Gio.UnixSocketAddressType.PATH);
let socketClient = new Gio.SocketClient();
socketClient.connect_async(address, this._cancellable,
Lang.bind(this, this._onCommandSocketConnected));
return false;
},
execute: function(subworkdir, buildData, repo, diskPath, cancellable) {
print("Testing disk " + diskPath.get_path());
this._buildData = buildData;
this._repo = repo;
this._subworkdir = subworkdir;
this._loop = GLib.MainLoop.new(null, true);
this._foundAllMessageIds = false;
this._complete = false;
this._failed = false;
this._journalStream = null;
this._journalDataStream = null;
this._commandConnectionAttempts = 0;
this._commandSocketDBus = null;
this._openedJournal = false;
this._readingJournal = false;
this._pendingRequiredMessageIds = {};
this._requestingScreenshot = false;
this._failMessageIds = {};
this._countPendingRequiredMessageIds = 0;
this._screenshotSerial = 0;
this._lastScreenshotChecksum = null;
this._qmpGreetingReceived = false;
this._qmpSocket = null;
this._qmpCommandOutstanding = [];
this._qmpConnectionAttempts = 0;
this._qmpCapabilitiesReceived = false;
print("Will wait for message IDs: " + JSON.stringify(this._testRequiredMessageIds));
for (let i = 0; i < this._testRequiredMessageIds.length; i++) {
this._pendingRequiredMessageIds[this._testRequiredMessageIds[i]] = true;
this._countPendingRequiredMessageIds += 1;
}
for (let i = 0; i < this._testFailedMessageIds.length; i++) {
this._failMessageIds[this._testFailedMessageIds[i]] = true;
}
this._cancellable = cancellable;
let osname = this._buildData['snapshot']['osname'];
// HACK
if (diskPath.get_basename().indexOf('x86_64') >= 0)
this._diskArch = 'x86_64';
else
this._diskArch = 'i686';
let qemuArgs = LibQA.getDefaultQemuOptions({ parallel: true });
let diskClone = subworkdir.get_child('testoverlay-' + diskPath.get_basename());
GSystem.shutil_rm_rf(diskClone, cancellable);
LibQA.createDiskSnapshot(diskPath, diskClone, cancellable);
let [gfmnt, mntdir] = LibQA.newReadWriteMount(diskClone, cancellable);
try {
LibQA.modifyBootloaderAppendKernelArgs(mntdir, ["console=ttyS0"], cancellable);
let [currentDir, currentEtcDir] = LibQA.getDeployDirs(mntdir, osname);
LibQA.injectExportJournal(currentDir, currentEtcDir, cancellable);
LibQA.injectTestUserCreation(currentDir, currentEtcDir, 'testuser', {}, cancellable);
LibQA.enableAutologin(currentDir, currentEtcDir, 'testuser', cancellable);
this._parentTask._prepareDisk(mntdir, this._diskArch, cancellable);
} finally {
gfmnt.umount(cancellable);
}
let consoleOutput = subworkdir.get_child('console.out');
let journalJson = subworkdir.get_child('journal.json');
let journalText = subworkdir.get_child('journal.txt');
GSystem.shutil_rm_rf(journalText, cancellable);
this._journalTextStream = journalText.replace(null, false,
Gio.FileCreateFlags.REPLACE_DESTINATION,
cancellable);
this._commandSocketPath = subworkdir.get_child('command.sock');
let commandSocketRelpath = subworkdir.get_relative_path(this._commandSocketPath);
qemuArgs.push.apply(qemuArgs, ['-drive', 'file=' + diskClone.get_path() + ',if=virtio',
'-vnc', 'none',
'-serial', 'file:' + consoleOutput.get_path(),
'-chardev', 'socket,id=charmonitor,path=qemu.monitor,server,nowait',
'-mon', 'chardev=charmonitor,id=monitor,mode=control',
'-device', 'virtio-serial',
'-chardev', 'file,id=journaljson,path=' + journalJson.get_path(),
'-device', 'virtserialport,chardev=journaljson,name=org.gnome.journaljson',
'-chardev', 'socket,id=commandchan,server,path=' + commandSocketRelpath,
'-device', 'virtserialport,chardev=commandchan,name=org.gnome.commandchan']);
let qemuContext = new GSystem.SubprocessContext({ argv: qemuArgs });
qemuContext.set_cwd(subworkdir.get_path());
let qemu = new GSystem.Subprocess({context: qemuContext});
this._qemu = qemu;
print("starting qemu : " + qemuArgs.join(' '));
qemu.init(cancellable);
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 250, Lang.bind(this, this._ensureQemuConnection));
qemu.wait(cancellable, Lang.bind(this, this._onQemuExited));
let journalMonitor = journalJson.monitor_file(0, cancellable);
journalMonitor.connect('changed', Lang.bind(this, this._onJournalChanged));
let commandConnectAttemptTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1,
Lang.bind(this, this._tryCommandConnection));
let timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, TIMEOUT_SECONDS,
Lang.bind(this, this._onTimeout));
// Let's only do a screenshot every 3 seconds, I think it's slowing things down...
let screenshotTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 3,
Lang.bind(this, this._idleScreenshot));
this._loop.run();
this._complete = true;
if (this._qemu) {
this._qemu.force_exit();
}
if (this._journalTextStream)
this._journalTextStream.close(null);
GLib.source_remove(timeoutId);
let [gfmnt, mntdir] = LibQA.newReadWriteMount(diskClone, cancellable);
try {
this._parentTask._postQemu(mntdir, cancellable);
} finally {
gfmnt.umount(cancellable);
}
//GSystem.shutil_rm_rf(diskClone, cancellable);
if (this._failed) {
throw new Error(this._failedMessage);
}
print("Completed testing of " + diskPath.get_basename());
}
});
const TestBase = new Lang.Class({
Name: 'TestBase',
Extends: Task.Task,
TaskDef: {
TaskName: "testbase",
TaskAfter: ['builddisks'],
},
TestTrees: ['-runtime'],
CompleteIdleWaitSeconds: 10,
BaseRequiredMessageIDs: ["39f53479d3a045ac8e11786248231fbf", // graphical.target
"f77379a8490b408bbe5f6940505a777b", // systemd-journald
],
BaseFailedMessageIDs: [],
RequiredMessageIDs: [],
FailedMessageIDs: [],
StatusMessageID: [],
CompletedTag: null,
_prepareDisk: function(mntdir, cancellable) {
// Nothing, intended for subclasses
},
// For subclasses
_onCommandChannelAsyncMessage: function(msgId, value) {
print("Received command async message " + msgId);
},
_handleMessage: function(message, cancellable) {
return false;
},
_onSuccess: function() {
},
_postQemu: function(mntdir, cancellable) {
},
_screenshotTaken: function(path) {
},
execute: function(cancellable) {
let imageDir = this.workdir.get_child('images');
let currentImages = imageDir.get_child('current');
let e = currentImages.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
cancellable);
let info;
let buildJson;
let disksToTest = [];
while ((info = e.next_file(cancellable)) != null) {
let name = info.get_name();
if (name.indexOf('build-') == 0 && JSUtil.stringEndswith(name, '.json')) {
buildJson = e.get_child(info);
continue;
}
if (!JSUtil.stringEndswith(name, '.qcow2'))
continue;
let matches = false;
for (let i = 0; i < this.TestTrees.length; i++) {
let tree = this.TestTrees[i];
if (JSUtil.stringEndswith(name, tree + '.qcow2')) {
matches = true;
break;
}
}
if (!matches) {
print("Skipping disk " + name + " not in " + JSON.stringify(this.TestTrees));
continue;
}
disksToTest.push(name);
}
e.close(null);
if (disksToTest.length == 0)
throw new Error("Didn't find any matching .qcow2 disks in " + currentImages.get_path());
this._buildData = null;
if (buildJson != null)
this._buildData = JSONUtil.loadJson(buildJson, cancellable);
for (let i = 0; i < disksToTest.length; i++) {
let name = disksToTest[i];
let workdirName = 'work-' + name.replace(/\.qcow2$/, '');
let subworkdir = Gio.File.new_for_path(workdirName);
GSystem.file_ensure_directory(subworkdir, true, cancellable);
let test = new TestOneDisk(this,
this.BaseRequiredMessageIDs.concat(this.RequiredMessageIDs),
this.BaseFailedMessageIDs.concat(this.FailedMessageIDs),
this.StatusMessageID);
test.execute(subworkdir, this._buildData, this.repo, currentImages.get_child(name), cancellable);
}
let buildData = this._buildData;
if (buildJson != null && this.CompletedTag !== null) {
let snapshot = buildData['snapshot'];
this.ostreeRepo.prepare_transaction(cancellable);
for (let targetName in buildData['targets']) {
let targetRev = buildData['targets'][targetName];
let lastSlash = targetName.lastIndexOf('/');
let testedRefName = snapshot['osname'] + '/' + this.CompletedTag + targetName.substr(lastSlash);
this.ostreeRepo.transaction_set_ref(null, testedRefName, targetRev);
print(Format.vprintf("Wrote ref: %s => %s", [testedRefName, targetRev]));
}
this.ostreeRepo.commit_transaction(cancellable);
} else {
print("No build json found, not tagging");
}
}
});

431
src/autobuilder/js/vcs.js Normal file
View File

@ -0,0 +1,431 @@
// Copyright (C) 2011,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.
const Params = imports.params;
const Format = imports.format;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GSystem = imports.gi.GSystem;
const ProcUtil = imports.procutil;
const BuildUtil = imports.buildutil;
const JSUtil = imports.jsutil;
function getMirrordir(mirrordir, keytype, uri, params) {
params = Params.parse(params, {prefix: ''});
let colon = uri.indexOf('://');
let scheme, rest;
if (colon >= 0) {
scheme = uri.substr(0, colon);
rest = uri.substr(colon+3);
} else {
scheme = 'file';
if (GLib.path_is_absolute(uri))
rest = uri.substr(1);
else
rest = uri;
}
let prefix = params.prefix ? params.prefix + '/' : '';
let relpath = prefix + keytype + '/' + scheme + '/' + rest;
return mirrordir.resolve_relative_path(relpath);
}
function _processCheckoutSubmodules(mirrordir, parentUri, cwd, cancellable) {
let lines = ProcUtil.runSyncGetOutputLines(['git', 'submodule', 'status'],
cancellable, {cwd: cwd});
let haveSubmodules = false;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (line == '') continue;
haveSubmodules = true;
line = line.substr(1);
let [subChecksum, subName, rest] = line.split(' ');
let configKey = Format.vprintf('submodule.%s.url', [subName]);
let subUrl = ProcUtil.runSyncGetOutputUTF8Stripped(['git', 'config', '-f', '.gitmodules', configKey],
cancellable, {cwd: cwd});
print("processing submodule " + subUrl);
if (subUrl.indexOf('../') == 0) {
subUrl = _makeAbsoluteUrl(parentUri, subUrl);
}
let localMirror = getMirrordir(mirrordir, 'git', subUrl);
ProcUtil.runSync(['git', 'config', configKey, 'file://' + localMirror.get_path()],
cancellable, {cwd:cwd});
ProcUtil.runSync(['git', 'submodule', 'update', '--init', subName], cancellable, {cwd: cwd});
_processCheckoutSubmodules(mirrordir, subUrl, cwd.get_child(subName), cancellable);
}
}
function getVcsCheckout(mirrordir, component, dest, cancellable, params) {
params = Params.parse(params, {overwrite: true,
quiet: false});
let [keytype, uri] = parseSrcKey(component['src']);
let revision;
let moduleMirror;
let addUpstream;
if (keytype == 'git' || keytype == 'local') {
revision = component['revision'];
moduleMirror = getMirrordir(mirrordir, keytype, uri);
addUpstream = true;
} else if (keytype == 'tarball') {
revision = 'tarball-import-' + component['checksum'];
moduleMirror = getMirrordir(mirrordir, 'tarball', component['name']);
addUpstream = false;
} else {
throw new Error("Unsupported src uri");
}
let checkoutdirParent = dest.get_parent();
GSystem.file_ensure_directory(checkoutdirParent, true, cancellable);
let tmpDest = checkoutdirParent.get_child(dest.get_basename() + '.tmp');
GSystem.shutil_rm_rf(tmpDest, cancellable);
let ftype = dest.query_file_type(Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
if (ftype == Gio.FileType.SYMBOLIC_LINK) {
GSystem.file_unlink(dest, cancellable);
} else if (ftype == Gio.FileType.DIRECTORY) {
if (params.overwrite) {
GSystem.shutil_rm_rf(dest, cancellable);
} else {
tmpDest = dest;
}
}
ftype = tmpDest.query_file_type(Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
if (ftype != Gio.FileType.DIRECTORY) {
ProcUtil.runSync(['git', 'clone', '-q', '--origin', 'localmirror',
'--no-checkout', moduleMirror.get_path(), tmpDest.get_path()], cancellable);
if (addUpstream)
ProcUtil.runSync(['git', 'remote', 'add', 'upstream', uri], cancellable, {cwd: tmpDest});
} else {
ProcUtil.runSync(['git', 'fetch', 'localmirror'], cancellable, {cwd: tmpDest});
}
ProcUtil.runSync(['git', 'checkout', '-q', revision], cancellable, {cwd: tmpDest});
_processCheckoutSubmodules(mirrordir, uri, tmpDest, cancellable);
if (!tmpDest.equal(dest)) {
GSystem.file_rename(tmpDest, dest, cancellable);
}
return dest;
}
function clean(keytype, checkoutdir, cancellable) {
ProcUtil.runSync(['git', 'clean', '-d', '-f', '-x'], cancellable,
{cwd: checkoutdir});
}
function parseSrcKey(srckey) {
let idx = srckey.indexOf(':');
if (idx < 0) {
throw new Error("Invalid SRC uri=" + srckey);
}
let keytype = srckey.substr(0, idx);
if (!(keytype == 'git' || keytype == 'local' || keytype == 'tarball')) {
throw new Error("Unsupported SRC uri=" + srckey);
}
let uri = srckey.substr(idx+1);
return [keytype, uri];
}
function checkoutPatches(mirrordir, patchdir, component, cancellable) {
let patches = component['patches'];
let [patchesKeytype, patchesUri] = parseSrcKey(patches['src']);
if (patchesKeytype == 'local')
return Gio.File.new_for_path(patchesUri);
else if (patchesKeytype != 'git')
throw new Error("Unhandled keytype " + patchesKeytype);
let patchesMirror = getMirrordir(mirrordir, patchesKeytype, patchesUri);
getVcsCheckout(mirrordir, patches, patchdir, cancellable,
{overwrite: true,
quiet: true});
return patchdir;
}
function getLastfetchPath(mirrordir, keytype, uri, branch) {
let mirror = getMirrordir(mirrordir, keytype, uri);
let branchSafename = branch.replace(/[\/.]/g, '_');
return mirror.get_parent().get_child(mirror.get_basename() + '.lastfetch-' + branchSafename);
}
function _listSubmodules(mirrordir, mirror, keytype, uri, branch, cancellable) {
let currentVcsVersion = ProcUtil.runSyncGetOutputUTF8(['git', 'rev-parse', branch], cancellable,
{cwd: mirror}).replace(/[ \n]/g, '');
let tmpCheckout = getMirrordir(mirrordir, keytype, uri, {prefix:'_tmp-checkouts'});
GSystem.shutil_rm_rf(tmpCheckout, cancellable);
GSystem.file_ensure_directory(tmpCheckout.get_parent(), true, cancellable);
ProcUtil.runSync(['git', 'clone', '-q', '--no-checkout', mirror.get_path(), tmpCheckout.get_path()], cancellable);
ProcUtil.runSync(['git', 'checkout', '-q', '-f', currentVcsVersion], cancellable,
{cwd: tmpCheckout});
let submodules = []
let lines = ProcUtil.runSyncGetOutputLines(['git', 'submodule', 'status'],
cancellable, {cwd: tmpCheckout});
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (line == '') continue;
line = line.substr(1);
let [subChecksum, subName, rest] = line.split(' ');
let subUrl = ProcUtil.runSyncGetOutputUTF8Stripped(['git', 'config', '-f', '.gitmodules',
Format.vprintf('submodule.%s.url', [subName])], cancellable,
{cwd: tmpCheckout});
submodules.push([subChecksum, subName, subUrl]);
}
GSystem.shutil_rm_rf(tmpCheckout, cancellable);
return submodules;
}
function _makeAbsoluteUrl(parent, relpath)
{
let origParent = parent;
let origRelpath = relpath;
if (JSUtil.stringEndswith(parent, '/'))
parent = parent.substr(0, parent.length - 1);
let methodIndex = parent.indexOf("://");
if (methodIndex == -1)
throw new Error("Invalid method");
let firstSlash = parent.indexOf('/', methodIndex + 3);
if (firstSlash == -1)
throw new Error("Invalid url");
let parentPath = parent.substr(firstSlash);
while (relpath.indexOf('../') == 0) {
let i = parentPath.lastIndexOf('/');
if (i < 0)
throw new Error("Relative path " + origRelpath + " is too long for parent " + origParent);
relpath = relpath.substr(3);
parentPath = parentPath.substr(0, i);
}
parent = parent.substr(0, firstSlash) + parentPath;
if (relpath.length == 0)
return parent;
return parent + '/' + relpath;
}
function ensureVcsMirror(mirrordir, component, cancellable,
params) {
params = Params.parse(params, { fetch: false,
fetchKeepGoing: false,
timeoutSec: 0 });
let [keytype, uri] = parseSrcKey(component['src']);
if (keytype == 'git' || keytype == 'local') {
let branch = component['branch'] || component['tag'];
return _ensureVcsMirrorGit(mirrordir, uri, branch, cancellable, params);
} else if (keytype == 'tarball') {
let name = component['name'];
let checksum = component['checksum'];
if (!checksum) {
throw new Error("Component " + name + " missing checksum attribute");
}
return _ensureVcsMirrorTarball(mirrordir, name, uri, checksum, cancellable, params);
} else {
throw new Error("Unhandled keytype=" + keytype);
}
}
function _ensureVcsMirrorGit(mirrordir, uri, branch, cancellable, params) {
let keytype = 'git';
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;
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,
{ logInitiation: true });
ProcUtil.runSync(['git', 'config', 'gc.auto', '0'], cancellable,
{ cwd: tmpMirror,
logInitiation: true });
GSystem.file_rename(tmpMirror, mirror, cancellable);
} else if (fetch) {
try {
ProcUtil.runSync(['git', 'fetch'], cancellable, { cwd: mirror,
logInitiation: true });
} catch (e) {
if (!params.fetchKeepGoing)
throw e;
}
}
let currentVcsVersion = ProcUtil.runSyncGetOutputUTF8(['git', 'rev-parse', branch], cancellable,
{cwd: mirror}).replace(/[ \n]/g, '');
let changed = currentVcsVersion != lastFetchContents;
if (changed) {
print(Format.vprintf("last fetch %s differs from branch %s", [lastFetchContents, currentVcsVersion]));
_listSubmodules(mirrordir, mirror, keytype, uri, branch, cancellable).forEach(function (elt) {
let [subChecksum, subName, subUrl] = elt;
print("Processing submodule " + subName + " at " + subChecksum + " from " + subUrl);
if (subUrl.indexOf('../') == 0) {
subUrl = _makeAbsoluteUrl(uri, subUrl);
print("Absolute URL: " + subUrl);
}
_ensureVcsMirrorGit(mirrordir, subUrl, subChecksum, cancellable, params);
});
}
if (changed || (fetch && params.timeoutSec > 0)) {
lastFetchPath.replace_contents(currentVcsVersion, null, false, 0, cancellable);
}
return mirror;
}
function _ensureVcsMirrorTarball(mirrordir, name, uri, checksum, cancellable, params) {
let fetch = params.fetch;
let mirror = getMirrordir(mirrordir, 'tarball', name);
let tmpMirror = mirror.get_parent().get_child(mirror.get_basename() + '.tmp');
if (!mirror.query_exists(cancellable)) {
GSystem.shutil_rm_rf(tmpMirror, cancellable);
GSystem.file_ensure_directory(tmpMirror, true, cancellable);
ProcUtil.runSync(['git', 'init', '--bare'], cancellable,
{ cwd: tmpMirror, logInitiation: true });
ProcUtil.runSync(['git', 'config', 'gc.auto', '0'], cancellable,
{ cwd: tmpMirror, logInitiation: true });
GSystem.file_rename(tmpMirror, mirror, cancellable);
}
let importTag = 'tarball-import-' + checksum;
let gitRevision = ProcUtil.runSyncGetOutputUTF8StrippedOrNull(['git', 'rev-parse', importTag],
cancellable, { cwd: mirror });
if (gitRevision != null) {
return mirror;
}
// First, we get a clone of the tarball git repo
let tmpCheckoutPath = mirrordir.get_child('tarball-cwd-' + name);
GSystem.shutil_rm_rf(tmpCheckoutPath, cancellable);
ProcUtil.runSync(['git', 'clone', mirror.get_path(), tmpCheckoutPath.get_path()], cancellable,
{ logInitiation: true });
// Now, clean the contents out
ProcUtil.runSync(['git', 'rm', '-r', '--ignore-unmatch', '.'], cancellable,
{ cwd: tmpCheckoutPath,
logInitiation: true });
// Download the tarball
let tmpPath = mirrordir.get_child('tarball-' + name);
GSystem.shutil_rm_rf(tmpPath, cancellable);
GSystem.file_ensure_directory(tmpPath.get_parent(), true, cancellable);
ProcUtil.runSync(['curl', '-L', '-v', '-o', tmpPath.get_path(), uri], cancellable,
{ logInitiation: true });
// And verify the checksum
let tarballData = GSystem.file_map_readonly(tmpPath, cancellable);
let actualChecksum = GLib.compute_checksum_for_bytes(GLib.ChecksumType.SHA256, tarballData);
if (actualChecksum != checksum) {
throw new Error("Downloaded " + uri + " expected checksum=" + checksum + " actual=" + actualChecksum);
}
let decompOpt = null;
if (JSUtil.stringEndswith(uri, '.xz'))
decompOpt = '--xz';
else if (JSUtil.stringEndswith(uri, '.bz2'))
decompOpt = '--bzip2';
else if (JSUtil.stringEndswith(uri, '.gz'))
decompOpt = '--gzip';
// Extract the tarball to our checkout
let args = ['tar', '-C', tmpCheckoutPath.get_path(), '-x'];
if (decompOpt !== null)
args.push(decompOpt);
args.push('-f');
args.push(tmpPath.get_path());
ProcUtil.runSync(args, cancellable, { logInitiation: true });
tarballData = null; // Clear this out in the hope the GC eliminates it
GSystem.file_unlink(tmpPath, cancellable);
// Automatically strip the first element if there's exactly one directory
let e = tmpCheckoutPath.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
cancellable);
let info;
let nFiles = 0;
let lastFileType = null;
let lastFile = null;
let lastInfo = null;
while ((info = e.next_file(cancellable)) != null) {
if (info.get_name() == '.git')
continue;
nFiles++;
lastFile = e.get_child(info);
lastInfo = info;
}
e.close(cancellable);
if (nFiles == 1 && lastInfo.get_file_type() == Gio.FileType.DIRECTORY) {
e = lastFile.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
cancellable);
while ((info = e.next_file(cancellable)) != null) {
let child = e.get_child(info);
if (!child.equal(lastFile))
GSystem.file_rename(child, tmpCheckoutPath.get_child(info.get_name()), cancellable);
}
lastFile.delete(cancellable);
e.close(cancellable);
}
let msg = 'Automatic import of ' + uri;
ProcUtil.runSync(['git', 'add', '.'],
cancellable, { cwd: tmpCheckoutPath });
ProcUtil.runSync(['git', 'commit', '-a', '--author=Automatic Tarball Importer <ostree-list@gnome.org>', '-m', msg ],
cancellable, { cwd: tmpCheckoutPath });
ProcUtil.runSync(['git', 'tag', '-m', msg, '-a', importTag ],
cancellable, { cwd: tmpCheckoutPath });
ProcUtil.runSync(['git', 'push', '--tags', 'origin', "master:master" ],
cancellable, { cwd: tmpCheckoutPath });
GSystem.shutil_rm_rf(tmpCheckoutPath, 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, component, cancellable, params) {
params = Params.parse(params, {keepGoing: false, timeoutSec: 0});
ensureVcsMirror(mirrordir, component, cancellable,
{ fetch:true,
fetchKeepGoing: params.keepGoing,
timeoutSec: params.timeoutSec });
}
function describeVersion(dirpath, branch) {
let args = ['git', 'describe', '--long', '--abbrev=42', '--always'];
if (branch) {
args.push(branch);
}
return ProcUtil.runSyncGetOutputUTF8(args, null, {cwd:dirpath}).replace(/[ \n]/g, '');
}

View File

@ -0,0 +1,194 @@
// 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 BuildUtil = imports.buildutil;
const FileUtil = imports.fileutil;
const JsonUtil = imports.jsonutil;
function relpathToVersion(relpath) {
let parts = relpath.split('/');
return parts[0] + parts[1] + parts[2] + '.' + parts[3];
}
const VersionedDir = new Lang.Class({
Name: 'VersionedDir',
_YMD_SERIAL_VERSION_RE: /^(\d+)(\d\d)(\d\d)\.(\d+)$/,
_YEAR_OR_SERIAL_VERSION_RE: /^(\d+)$/,
_MONTH_OR_DAY_VERSION_RE: /^\d\d$/,
_init: function(path) {
this.path = path;
this._cachedResults = null;
GSystem.file_ensure_directory(this.path, true, null);
},
_createPathForParsedVersion: function(year, month, day, serial) {
return this.path.resolve_relative_path(Format.vprintf('%s/%s/%s/%s',
[year, month, day, serial]));
},
createPathForVersion: function(ymdSerial) {
let match = this._YMD_SERIAL_VERSION_RE.exec(ymdSerial);
if (!match) throw new Error();
return this.path.resolve_relative_path(match[1] + '/' + match[2] + '/' +
match[3] + '/' + match[4]);
},
_iterateChildrenMatching: function(dir, pattern, callback, cancellable) {
let e = dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable);
let info;
while ((info = e.next_file(cancellable)) != null) {
let srcpath = e.get_child(info);
let name = info.get_name();
let match = pattern.exec(name);
if (!match)
continue;
callback(srcpath, match, cancellable);
}
e.close(null);
},
_loadYear: function(yeardir, results, cancellable) {
this._iterateChildrenMatching(yeardir, this._MONTH_OR_DAY_VERSION_RE,
Lang.bind(this, function(srcpath, match, cancellable) {
this._loadMonth(srcpath, results, cancellable);
}), cancellable);
},
_loadMonth: function(monthdir, results, cancellable) {
this._iterateChildrenMatching(monthdir, this._MONTH_OR_DAY_VERSION_RE,
Lang.bind(this, function(srcpath, match, cancellable) {
this._loadDay(srcpath, results, cancellable);
}), cancellable);
},
_loadDay: function(daydir, results, cancellable) {
this._iterateChildrenMatching(daydir, this._YEAR_OR_SERIAL_VERSION_RE,
Lang.bind(this, function(srcpath, match, cancellable) {
results.push(this.pathToVersion(srcpath));
}), cancellable);
},
_convertLegacyLayout: function(cancellable) {
this._iterateChildrenMatching(this.path, this._YMD_SERIAL_VERSION_RE,
Lang.bind(this, function(srcpath, match, cancellable) {
let path = this._createPathForParsedVersion(match[1], match[2], match[3], match[4]);
print("convert " + srcpath.get_path() + " -> " + path.get_path());
GSystem.file_ensure_directory(path.get_parent(), true, cancellable);
GSystem.file_rename(srcpath, path, cancellable);
}), cancellable);
},
pathToVersion: function(path) {
return relpathToVersion(this.path.get_relative_path(path));
},
loadVersions: function(cancellable) {
if (this._cachedResults !== null)
return this._cachedResults;
let results = [];
this._convertLegacyLayout(cancellable);
this._iterateChildrenMatching(this.path, this._YEAR_OR_SERIAL_VERSION_RE,
Lang.bind(this, function(srcpath, match, cancellable) {
this._loadYear(srcpath, results, cancellable);
}), cancellable);
results.sort(BuildUtil.compareVersions);
this._cachedResults = results;
return results;
},
currentVersion: function(cancellable) {
let versions = this.loadVersions(cancellable);
if (versions.length > 0)
return versions[versions.length - 1];
return null;
},
_makeDirUpdateIndex: function(path, cancellable) {
let relpath = this.path.get_relative_path(path);
if (relpath == null) {
GSystem.file_ensure_directory(path, true, cancellable);
return;
}
let parent = path.get_parent();
this._makeDirUpdateIndex(parent, cancellable);
let created = false;
try {
path.make_directory(cancellable);
created = true;
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
throw e;
}
let indexJsonPath = parent.get_child('index.json');
if (!created)
created = !indexJsonPath.query_exists(null);
if (created) {
let childNames = [];
FileUtil.walkDir(parent, { depth: 1,
fileType: Gio.FileType.DIRECTORY },
Lang.bind(this, function(filePath, cancellable) {
childNames.push(filePath.get_basename());
}), cancellable);
JsonUtil.writeJsonFileAtomic(indexJsonPath,
{ 'subdirs': childNames }, cancellable);
}
},
allocateNewVersion: function(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 versions = this.loadVersions(cancellable);
let newVersion = null;
if (versions.length > 0) {
let last = versions[versions.length-1];
let match = this._YMD_SERIAL_VERSION_RE.exec(last);
if (!match) throw new Error();
let lastYmd = match[1] + match[2] + match[3];
let lastSerial = match[4];
if (lastYmd == currentYmd) {
newVersion = currentYmd + '.' + (parseInt(lastSerial) + 1);
}
}
if (newVersion === null)
newVersion = currentYmd + '.0';
let path = this.createPathForVersion(newVersion);
this._makeDirUpdateIndex(path, cancellable);
this._cachedResults.push(newVersion);
return path;
},
deleteCurrentVersion: function(cancellable) {
let versions = this.loadVersions(cancellable);
if (versions.length == 0)
throw new Error();
let last = versions.pop();
let path = this.createPathForVersion(last);
GSystem.shutil_rm_rf(path, cancellable);
}
});

View File

@ -0,0 +1,28 @@
#!/bin/sh
# 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.
jsdir=@pkgdatadir@/js
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
export OSTBUILD_DATADIR=@pkgdatadir@
export OSTBUILD_LIBDIR=@pkglibdir@
exec $OSTBUILD_GDB gjs -I "${jsdir}" "${jsdir}/main.js" "$@"