Extending the Installer

The Cloud9 Installer can be extended with custom package managers that add a new way to install a package or manipulate the workspace.

A package manager is a simple plugin with an execute() method that takes a task argument and an options object.

In the example below the first object is the options object and in this case the npm package manager gets its execute() method called with "[email protected]" as its task.

session.install({
    "cwd": "~/.c9"
}, {
    "npm": "[email protected]"
});

In the following example there are multiple items for the npm package manager. However, the execute() method will be called once for each.

session.install({
    "cwd": "~/.c9"
}, {
    "npm": ["[email protected]", "[email protected]"]
});

Creating a Package Manager

A basic package manager starts with the following template.

define(function(require, exports, module) {
    main.consumes = ["Plugin", "installer", "proc"];
    main.provides = ["installer.name"];
    return main;
    
    function main(options, imports, register) {
        var Plugin = imports.Plugin;
        var installer = imports.installer;
        var proc = imports.proc;
        
        var binBash = options.binBash || "bash";
        
        var plugin = new Plugin("Your Name", main.consumes);
        
        function execute(task, options, onData, callback) {
            
        }
        
        var available;
        function isAvailable(callback){
            
        }
        
        plugin.on("load", function() {
            installer.addPackageManager("name", plugin);
            installer.addPackageManagerAlias("name", "othername");
        });
        plugin.on("unload", function() {
            installer.removePackageManager("name");
            available = undefined;
        });
        
        plugin.freezePublicAPI({ execute: execute, isAvailable: isAvailable });
        
        register(null, {
            "installer.name": plugin
        });
    }
});

isAvailable

The isAvailable() method is called by the installer to confirm that this package manager is available on the workspace that it is run on. An implementor must cache the result of this function.

The following example is taken from the ubuntu package manager. It checks whether apt-get is installed on the system and caches the result in the available variable.

var available;
function isAvailable(callback){
    if (typeof available != "undefined")
        return callback(available);
    
    proc.execFile("which", { args: ["apt-get"] }, function(err, stdout){
        if (err) return callback(false);
        
        available = stdout.length > 0;
        callback(available);
    });
}

execute

As already mentioned above, the execute() method executes a single task. It takes four arguments. The task contains the string or object that describes the single task to execute. The options object contains any options specified by the user. The onData function should be called to stream any data that can be outputted in the installation log. Lastly the callback must be called when execution is stopped, either successfully or not.

The following example is again from the ubuntu package manager. As you can see below, the package manager should use proc.pty to start processes if they are long running.

function execute(task, options, onData, callback) {
    var script = 'set -e\n'
        + 'sudo apt-get install ' + task;
    
    proc.pty(binBash, {
        args: ["-c", script],
        cwd: options.cwd || null
    }, function(err, pty){
        if (err) return callback(err);
        
        // Pipe the data to the onData function
        pty.on("data", function(chunk){
            onData(chunk, pty);
        });
        
        // When process exits call callback
        pty.on("exit", function(code){
            if (!code) callback();
            else callback(new Error("Failed. Exit code " + code));
        });
    });
}