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;