diff --git a/README.md b/README.md
index bdb2224d17d..bbb276e91e9 100644
--- a/README.md
+++ b/README.md
@@ -1,50 +1,69 @@
-# compass
+# compass [![][travis_img]][travis_url]
-Explore your MongoDB.
+> Explore your MongoDB.
-## Developing
+## Development
1. Follow the setup instructions for [OSX][setup-osx], [Windows][setup-windows] or [Linux][setup-linux].
-2. Clone this repo
+2. Run `git clone git@github.com:10gen/compass.git ~/compass` to get the source code
3. Run `npm install` to install dependencies
-4. Run `npm start` to launch
+4. Run `npm start` to build the app and launch it
-## Modules
+Already setup and prefer a simple copy and paste?
+
+```bash
+git clone git@github.com:10gen/compass.git ~/compass;
+cd ~/compass;
+npm install;
+npm start;
+```
+
+## Key Modules
- - Compass
+
compass
-
The default Ampersand.js single-page application people actually interact with.
- ScoutClientMixin
- connects the models to
- scout-client.
- - scout-brain
+
scout-client
-
- Needs to be broken down into topic based models but for now, this is where
- all the business logic code lives we want to share between modules running
- in the browser, nodejs, or electron.
+ Provides a clean API for `compass` to talk to `scout-server` that works in the browser, nodejs, or electron.
- - scout-client
+
scout-server
-
- Provides a clean API for scout-server
- that works in the browser, nodejs, or electron.
+ An express.js application which provides REST and socket.io endpoints
+ to the mongodb node.js driver.
- - scout-server
+
mongodb-connection-model
-
- An express.js application which provides REST and socket.io connectivity
- to the mongodb node.js driver.
+ A shared Ampersand.js model used by `compass`, `scout-client`, and `scout-server` that encapsulates
+ all of the business logic for generating valid parameters to hand to the driver to connect to MongoDB.
+
+
mongodb-collection-sample
+ -
+ Provides a single interface for `scout-server` to request a sample of documents from a collection that automatically uses the `$sample` operator if available, falling back to a client-side reservoir sample.
+
+
mongodb-schema
+ -
+ `compass` uses `scout-client` to get a sample of documents from `scout-server` via `mongodb-collection-sample` which is piped into `mongodb-schema` to create a probabilistic representation of what the schema for a given collection most likely is.
+
## Building Releases
-To compile electron + the app and the installer for your current platform:
+After you've made some local changes, the next thing you'll probably want to do
+is create an artifact to share. There is only one command you need to run to compile the app,
+sign it if the signing certificate is available on your machine, and generate a single file
+installer for your current platform:
```bash
-npm run release
+cd ~/compass;
+npm run release;
```
[setup-osx]: https://github.com/mongodb-js/mongodb-js/blob/master/docs/setup.md#osx-setup
[setup-windows]: https://github.com/mongodb-js/mongodb-js/blob/master/docs/setup.md#windows-setup
[setup-linux]: https://github.com/mongodb-js/mongodb-js/blob/master/docs/setup.md#linux-setup
+[travis_img]: https://magnum.travis-ci.com/10gen/compass.svg?token=q2zsnxCbboarF6KYRYxM&branch=master
+[travis_url]: https://magnum.travis-ci.com/10gen/compass
diff --git a/package.json b/package.json
index f56316c946d..9c5d68953b8 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"fonts/*"
],
"browserify": {
+ "debug": true,
"entries": [
"./src/index.js"
]
@@ -92,6 +93,7 @@
"bootstrap": "https://github.com/twbs/bootstrap/archive/v3.3.5.tar.gz",
"browserify": "^10.2.4",
"bugsnag-js": "^2.4.8",
+ "chalk": "^1.1.1",
"d3": "^3.5.5",
"del": "^1.2.0",
"domready": "^1.0.8",
@@ -101,6 +103,7 @@
"eslint": "^0.24.1",
"eslint-config-mongodb-js": "^0.1.4",
"event-stream": "^3.3.1",
+ "figures": "^1.4.0",
"font-awesome": "https://github.com/FortAwesome/Font-Awesome/archive/v4.3.0.tar.gz",
"glob": "^5.0.14",
"gulp": "^3.9.0",
diff --git a/tasks/darwin.js b/tasks/darwin.js
index acc86a007ed..bb872debbcd 100644
--- a/tasks/darwin.js
+++ b/tasks/darwin.js
@@ -1,9 +1,14 @@
var path = require('path');
var pkg = require(path.resolve(__dirname, '../package.json'));
var fs = require('fs');
-var cp = require('child_process');
+var run = require('./run');
+var format = require('util').format;
+var chalk = require('chalk');
+var figures = require('figures');
var series = require('run-series');
var _ = require('lodash');
+var packager = require('electron-packager');
+var createDMG = require('electron-installer-dmg');
var debug = require('debug')('scout:tasks:darwin');
@@ -11,9 +16,6 @@ var NAME = pkg.product_name;
var PACKAGE = path.join('dist', NAME + '-darwin-x64');
var APP_PATH = path.join(PACKAGE, NAME + '.app');
-var packager = require('electron-packager');
-var createDMG = require('electron-installer-dmg');
-
module.exports.ELECTRON = path.join(APP_PATH, 'Contents', 'MacOS', 'Electron');
module.exports.RESOURCES = path.join(APP_PATH, 'Contents', 'Resources');
@@ -29,7 +31,6 @@ var PACKAGER_CONFIG = {
prune: true,
'app-bundle-id': 'com.mongodb.compass',
'app-version': pkg.version,
- sign: '90E39AA7832E95369F0FC6DAF823A04DFBD9CF7A',
protocols: [
{
name: 'MongoDB Prototcol',
@@ -38,11 +39,6 @@ var PACKAGER_CONFIG = {
]
};
-// Adjust config via environment variables
-if (process.env.SCOUT_INSTALLER_UNSIGNED !== undefined) {
- PACKAGER_CONFIG.sign = null;
-}
-
// @todo (imlucas): Standardize `electron-installer-dmg`
// options w/ `electron-installer-squirrel-windows`.
var INSTALLER_CONFIG = {
@@ -69,39 +65,127 @@ var INSTALLER_CONFIG = {
]
};
-module.exports.build = function(done) {
- fs.exists(APP_PATH, function(exists) {
- if (exists) {
- debug('.app already exists. skipping packager run.');
- return done();
+var CODESIGN_IDENTITY_COMMON_NAME = 'Developer ID Application: Matt Kangas (ZD3CL9MT3L)';
+var CODESIGN_IDENTITY_SHA1 = '90E39AA7832E95369F0FC6DAF823A04DFBD9CF7A';
+
+/**
+ * Checks if the current environment can actually sign builds.
+ * If signing can be done, `electron-packager`'s config will
+ * be updated to sign artifacts. If not, gracefully degrade
+ *
+ * @param {Function} fn - Callback.
+ */
+function addCodesignIdentityIfAvailable(fn) {
+ run('certtool', ['y'], function(err, output) {
+ if (err) {
+ debug('Failed to list certificates. Build will not be signed.');
+ fn();
+ return;
}
- debug('running packager to create electron binaries...');
- packager(PACKAGER_CONFIG, done);
+ if (output.indexOf(CODESIGN_IDENTITY_COMMON_NAME) === -1) {
+ debug('Signing identity `%s` not detected. Build will not be signed.',
+ CODESIGN_IDENTITY_COMMON_NAME);
+ fn();
+ return;
+ }
+
+ PACKAGER_CONFIG.sign = CODESIGN_IDENTITY_SHA1;
+ debug('The signing identity `%s` is available! '
+ + 'This build will be signed!', CODESIGN_IDENTITY_COMMON_NAME);
+
+ console.log(chalk.green.bold(figures.tick),
+ format(' This build will be signed using the `%s` signing identity',
+ CODESIGN_IDENTITY_COMMON_NAME));
+ fn();
});
-};
+}
+
+module.exports.build = function(done) {
+ addCodesignIdentityIfAvailable(function(err) {
+ if (err) return done(err);
-var verify = function(done) {
- var cmd = 'codesign --verify "' + APP_PATH + '"';
- debug('Verifying `%s` has been signed...', APP_PATH);
- cp.exec(cmd, done);
+ fs.exists(APP_PATH, function(exists) {
+ if (exists && process.env.NODE_ENV !== 'production') {
+ debug('.app already exists. skipping packager run.');
+ return done();
+ }
+ debug('running electron-packager...');
+ packager(PACKAGER_CONFIG, done);
+ });
+ });
};
-module.exports.installer = function(done) {
- debug('creating installer...');
+/**
+ * If the app is supposed to be signed, verify that
+ * the signing was actually completed correctly.
+ * If signing is not available, print helpful details
+ * on working with unsigned builds.
+ *
+ * @param {Function} done - Callback which receives `(err)`.
+ */
+function verify(done) {
+ if (!PACKAGER_CONFIG.sign) {
+ console.error(chalk.yellow.bold(figures.warning),
+ ' User confusion ahead!');
- var tasks = [];
- if (PACKAGER_CONFIG.sign) {
- tasks.push(verify);
+ console.error(chalk.gray(
+ ' The default preferences for OSX Gatekeeper will not',
+ 'allow users to run unsigned applications.'));
+
+ console.error(chalk.gray(
+ ' However, we\'re going to continue building',
+ 'the app and an installer because you\'re most likely'));
+
+ console.error(chalk.gray(
+ ' a developer trying to test',
+ 'the app\'s installation process.'));
+
+ console.error(chalk.gray(
+ ' For more information on OSX Gatekeeper and how to change your',
+ 'system preferences to run unsigned applications,'));
+ console.error(chalk.gray(' please see',
+ 'https://support.apple.com/en-us/HT202491'));
+ debug('Build is not signed. Skipping codesign verification.');
+ process.nextTick(done);
+ return;
}
- tasks.push(_.partial(createDMG, INSTALLER_CONFIG));
+ debug('Verifying `%s` has been signed correctly...', APP_PATH);
+ run('codesign', ['--verify', APP_PATH], function(err) {
+ if (err) {
+ err = new Error('App is not correctly signed');
+ done(err);
+ return;
+ }
+ debug('Verified that the app has been signed correctly!');
+ done();
+ });
+}
+
+/**
+ * Package the application as a single `.DMG` file which
+ * is the OSX equivalent of a `Setup.exe` installer.
+ *
+ * @param {Function} done - Callback which receives `(err)`.
+ */
+module.exports.installer = function(done) {
+ debug('creating installer...');
+
+ var tasks = [
+ verify,
+ _.partial(createDMG, INSTALLER_CONFIG)
+ ];
series(tasks, function(err) {
if (err) {
- console.error(err.stack);
+ console.error(chalk.red.bold(figures.cross),
+ 'Failed to create DMG installer:', err.message);
+ console.error(chalk.gray(err.stack));
return done(err);
}
- console.log('Installer created!');
+ console.log(chalk.green.bold(figures.tick),
+ ' DMG installer written to',
+ path.join(INSTALLER_CONFIG.out, INSTALLER_CONFIG.name + '.dmg'));
done();
});
};
diff --git a/tasks/run.js b/tasks/run.js
new file mode 100644
index 00000000000..1fc9a033d15
--- /dev/null
+++ b/tasks/run.js
@@ -0,0 +1,108 @@
+var fs = require('fs');
+var format = require('util').format;
+var spawn = require('child_process').spawn;
+var which = require('which');
+var debug = require('debug')('scout:tasks:run');
+
+/**
+ * Use me when you want to run an external command instead
+ * of using `child_process` directly because I'll handle
+ * lots of platform edge cases for you and provide
+ * nice debugging output when things go wrong!
+ *
+ * @example
+ * var run = require('./tasks/run');
+ * var args = ['--verify', require('./tasks/darwin').APP_PATH];
+ * run('codesign', args, function(err){
+ * if(err){
+ * console.error('codesign verification failed!');
+ * process.exit(1);
+ * }
+ * console.log('codesign verification succeeded!');
+ * });
+ *
+ * @param {String} cmd - The bin name of your command, e.g. `grep`.
+ * @param {Array} [args] - Arguments to pass to the command [Default `[]`].
+ * @param {Object} [opts] - Options to pass to `child_process.spawn` [Default `{}`].
+ * @param {Function} fn - Callback which recieves `(err, output)`.
+ */
+function run(cmd, args, opts, fn) {
+ if (typeof opts === 'function') {
+ fn = opts;
+ opts = {};
+ }
+
+ if (typeof args === 'function') {
+ fn = args;
+ args = [];
+ opts = {};
+ }
+
+ getBinPath(cmd, function(err, bin) {
+ if (err) return fn(err);
+
+ debug('running %j', {
+ cmd: cmd,
+ args: args,
+ opts: opts
+ });
+ var output = [];
+
+ var proc = spawn(bin, args, opts);
+ proc.stdout.on('data', function(buf) {
+ debug(' %s> %s', cmd, buf.toString('utf-8'));
+ output.push(buf);
+ });
+ proc.stderr.on('data', function(buf) {
+ debug(' %s> %s', cmd, buf.toString('utf-8'));
+ output.push(buf);
+ });
+
+ proc.on('exit', function(code) {
+ if (code !== 0) {
+ debug('command failed! %j', {
+ cmd: cmd,
+ bin: bin,
+ args: args,
+ opts: opts,
+ code: code
+ });
+ fn(new Error('Command failed! '
+ + 'Please try again with debugging enabled.'));
+ return;
+ }
+ debug('completed! %j', {
+ cmd: cmd,
+ bin: bin,
+ args: args,
+ opts: opts,
+ code: code
+ });
+
+ fn(null, Buffer.concat(output).toString('utf-8'));
+ });
+ });
+}
+
+/**
+ * Gets the absolute path for a `cmd`.
+ * @param {String} cmd - e.g. `codesign`.
+ * @param {Function} fn - Callback which receives `(err, binPath)`.
+ * @return {void}
+ */
+function getBinPath(cmd, fn) {
+ which(cmd, function(err, bin) {
+ if (err) return fn(err);
+
+ fs.exists(bin, function(exists) {
+ if (!exists) {
+ return fn(new Error(format(
+ 'Expected file for `%s` does not exist at `%s`',
+ cmd, bin)));
+ }
+ fn(null, bin);
+ });
+ });
+}
+
+module.exports = run;