Skip to content

Commit 7f7f029

Browse files
committed
⚡ 🎉 auto detect if you can sign builds
Rather than having to set an environment variable to say you don’t have the cert, let the build tooling figure it out for you and log more useful messages along the way. If you have the certificate, you’ll now see: ![](https://cldup.com/--k6ry1Av7-2000x2000.png) And if you don’t, you’ll no longer see a really confusing stack trace but instead: ![](https://cldup.com/v5gx_zbkUo-3000x3000.png)
1 parent 83b342b commit 7f7f029

File tree

3 files changed

+224
-30
lines changed

3 files changed

+224
-30
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"bootstrap": "https://github.com/twbs/bootstrap/archive/v3.3.5.tar.gz",
9494
"browserify": "^10.2.4",
9595
"bugsnag-js": "^2.4.8",
96+
"chalk": "^1.1.1",
9697
"d3": "^3.5.5",
9798
"del": "^1.2.0",
9899
"domready": "^1.0.8",
@@ -102,6 +103,7 @@
102103
"eslint": "^0.24.1",
103104
"eslint-config-mongodb-js": "^0.1.4",
104105
"event-stream": "^3.3.1",
106+
"figures": "^1.4.0",
105107
"font-awesome": "https://github.com/FortAwesome/Font-Awesome/archive/v4.3.0.tar.gz",
106108
"glob": "^5.0.14",
107109
"gulp": "^3.9.0",

tasks/darwin.js

Lines changed: 114 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
var path = require('path');
22
var pkg = require(path.resolve(__dirname, '../package.json'));
33
var fs = require('fs');
4-
var cp = require('child_process');
4+
var run = require('./run');
5+
var format = require('util').format;
6+
var chalk = require('chalk');
7+
var figures = require('figures');
58
var series = require('run-series');
69
var _ = require('lodash');
10+
var packager = require('electron-packager');
11+
var createDMG = require('electron-installer-dmg');
712

813
var debug = require('debug')('scout:tasks:darwin');
914

1015
var NAME = pkg.product_name;
1116
var PACKAGE = path.join('dist', NAME + '-darwin-x64');
1217
var APP_PATH = path.join(PACKAGE, NAME + '.app');
1318

14-
var packager = require('electron-packager');
15-
var createDMG = require('electron-installer-dmg');
16-
1719
module.exports.ELECTRON = path.join(APP_PATH, 'Contents', 'MacOS', 'Electron');
1820
module.exports.RESOURCES = path.join(APP_PATH, 'Contents', 'Resources');
1921

@@ -29,7 +31,6 @@ var PACKAGER_CONFIG = {
2931
prune: true,
3032
'app-bundle-id': 'com.mongodb.compass',
3133
'app-version': pkg.version,
32-
sign: '90E39AA7832E95369F0FC6DAF823A04DFBD9CF7A',
3334
protocols: [
3435
{
3536
name: 'MongoDB Prototcol',
@@ -38,11 +39,6 @@ var PACKAGER_CONFIG = {
3839
]
3940
};
4041

41-
// Adjust config via environment variables
42-
if (process.env.SCOUT_INSTALLER_UNSIGNED !== undefined) {
43-
PACKAGER_CONFIG.sign = null;
44-
}
45-
4642
// @todo (imlucas): Standardize `electron-installer-dmg`
4743
// options w/ `electron-installer-squirrel-windows`.
4844
var INSTALLER_CONFIG = {
@@ -69,39 +65,127 @@ var INSTALLER_CONFIG = {
6965
]
7066
};
7167

72-
module.exports.build = function(done) {
73-
fs.exists(APP_PATH, function(exists) {
74-
if (exists) {
75-
debug('.app already exists. skipping packager run.');
76-
return done();
68+
var CODESIGN_IDENTITY_COMMON_NAME = 'Developer ID Application: Matt Kangas (ZD3CL9MT3L)';
69+
var CODESIGN_IDENTITY_SHA1 = '90E39AA7832E95369F0FC6DAF823A04DFBD9CF7A';
70+
71+
/**
72+
* Checks if the current environment can actually sign builds.
73+
* If signing can be done, `electron-packager`'s config will
74+
* be updated to sign artifacts. If not, gracefully degrade
75+
*
76+
* @param {Function} fn - Callback.
77+
*/
78+
function addCodesignIdentityIfAvailable(fn) {
79+
run('certtool', ['y'], function(err, output) {
80+
if (err) {
81+
debug('Failed to list certificates. Build will not be signed.');
82+
fn();
83+
return;
7784
}
78-
debug('running packager to create electron binaries...');
79-
packager(PACKAGER_CONFIG, done);
85+
if (output.indexOf(CODESIGN_IDENTITY_COMMON_NAME) === -1) {
86+
debug('Signing identity `%s` not detected. Build will not be signed.',
87+
CODESIGN_IDENTITY_COMMON_NAME);
88+
fn();
89+
return;
90+
}
91+
92+
PACKAGER_CONFIG.sign = CODESIGN_IDENTITY_SHA1;
93+
debug('The signing identity `%s` is available! '
94+
+ 'This build will be signed!', CODESIGN_IDENTITY_COMMON_NAME);
95+
96+
console.log(chalk.green.bold(figures.tick),
97+
format(' This build will be signed using the `%s` signing identity',
98+
CODESIGN_IDENTITY_COMMON_NAME));
99+
fn();
80100
});
81-
};
101+
}
102+
103+
module.exports.build = function(done) {
104+
addCodesignIdentityIfAvailable(function(err) {
105+
if (err) return done(err);
82106

83-
var verify = function(done) {
84-
var cmd = 'codesign --verify "' + APP_PATH + '"';
85-
debug('Verifying `%s` has been signed...', APP_PATH);
86-
cp.exec(cmd, done);
107+
fs.exists(APP_PATH, function(exists) {
108+
if (exists && process.env.NODE_ENV !== 'production') {
109+
debug('.app already exists. skipping packager run.');
110+
return done();
111+
}
112+
debug('running electron-packager...');
113+
packager(PACKAGER_CONFIG, done);
114+
});
115+
});
87116
};
88117

89-
module.exports.installer = function(done) {
90-
debug('creating installer...');
118+
/**
119+
* If the app is supposed to be signed, verify that
120+
* the signing was actually completed correctly.
121+
* If signing is not available, print helpful details
122+
* on working with unsigned builds.
123+
*
124+
* @param {Function} done - Callback which receives `(err)`.
125+
*/
126+
function verify(done) {
127+
if (!PACKAGER_CONFIG.sign) {
128+
console.error(chalk.yellow.bold(figures.warning),
129+
' User confusion ahead!');
91130

92-
var tasks = [];
93-
if (PACKAGER_CONFIG.sign) {
94-
tasks.push(verify);
131+
console.error(chalk.gray(
132+
' The default preferences for OSX Gatekeeper will not',
133+
'allow users to run unsigned applications.'));
134+
135+
console.error(chalk.gray(
136+
' However, we\'re going to continue building',
137+
'the app and an installer because you\'re most likely'));
138+
139+
console.error(chalk.gray(
140+
' a developer trying to test',
141+
'the app\'s installation process.'));
142+
143+
console.error(chalk.gray(
144+
' For more information on OSX Gatekeeper and how to change your',
145+
'system preferences to run unsigned applications,'));
146+
console.error(chalk.gray(' please see',
147+
'https://support.apple.com/en-us/HT202491'));
148+
debug('Build is not signed. Skipping codesign verification.');
149+
process.nextTick(done);
150+
return;
95151
}
96152

97-
tasks.push(_.partial(createDMG, INSTALLER_CONFIG));
153+
debug('Verifying `%s` has been signed correctly...', APP_PATH);
154+
run('codesign', ['--verify', APP_PATH], function(err) {
155+
if (err) {
156+
err = new Error('App is not correctly signed');
157+
done(err);
158+
return;
159+
}
160+
debug('Verified that the app has been signed correctly!');
161+
done();
162+
});
163+
}
164+
165+
/**
166+
* Package the application as a single `.DMG` file which
167+
* is the OSX equivalent of a `Setup.exe` installer.
168+
*
169+
* @param {Function} done - Callback which receives `(err)`.
170+
*/
171+
module.exports.installer = function(done) {
172+
debug('creating installer...');
173+
174+
var tasks = [
175+
verify,
176+
_.partial(createDMG, INSTALLER_CONFIG)
177+
];
98178

99179
series(tasks, function(err) {
100180
if (err) {
101-
console.error(err.stack);
181+
console.error(chalk.red.bold(figures.cross),
182+
'Failed to create DMG installer:', err.message);
183+
console.error(chalk.gray(err.stack));
102184
return done(err);
103185
}
104-
console.log('Installer created!');
186+
console.log(chalk.green.bold(figures.tick),
187+
' DMG installer written to',
188+
path.join(INSTALLER_CONFIG.out, INSTALLER_CONFIG.name + '.dmg'));
105189
done();
106190
});
107191
};

tasks/run.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
var fs = require('fs');
2+
var format = require('util').format;
3+
var spawn = require('child_process').spawn;
4+
var which = require('which');
5+
var debug = require('debug')('scout:tasks:run');
6+
7+
/**
8+
* Use me when you want to run an external command instead
9+
* of using `child_process` directly because I'll handle
10+
* lots of platform edge cases for you and provide
11+
* nice debugging output when things go wrong!
12+
*
13+
* @example
14+
* var run = require('./tasks/run');
15+
* var args = ['--verify', require('./tasks/darwin').APP_PATH];
16+
* run('codesign', args, function(err){
17+
* if(err){
18+
* console.error('codesign verification failed!');
19+
* process.exit(1);
20+
* }
21+
* console.log('codesign verification succeeded!');
22+
* });
23+
*
24+
* @param {String} cmd - The bin name of your command, e.g. `grep`.
25+
* @param {Array} [args] - Arguments to pass to the command [Default `[]`].
26+
* @param {Object} [opts] - Options to pass to `child_process.spawn` [Default `{}`].
27+
* @param {Function} fn - Callback which recieves `(err, output)`.
28+
*/
29+
function run(cmd, args, opts, fn) {
30+
if (typeof opts === 'function') {
31+
fn = opts;
32+
opts = {};
33+
}
34+
35+
if (typeof args === 'function') {
36+
fn = args;
37+
args = [];
38+
opts = {};
39+
}
40+
41+
getBinPath(cmd, function(err, bin) {
42+
if (err) return fn(err);
43+
44+
debug('running %j', {
45+
cmd: cmd,
46+
args: args,
47+
opts: opts
48+
});
49+
var output = [];
50+
51+
var proc = spawn(bin, args, opts);
52+
proc.stdout.on('data', function(buf) {
53+
debug(' %s> %s', cmd, buf.toString('utf-8'));
54+
output.push(buf);
55+
});
56+
proc.stderr.on('data', function(buf) {
57+
debug(' %s> %s', cmd, buf.toString('utf-8'));
58+
output.push(buf);
59+
});
60+
61+
proc.on('exit', function(code) {
62+
if (code !== 0) {
63+
debug('command failed! %j', {
64+
cmd: cmd,
65+
bin: bin,
66+
args: args,
67+
opts: opts,
68+
code: code
69+
});
70+
fn(new Error('Command failed! '
71+
+ 'Please try again with debugging enabled.'));
72+
return;
73+
}
74+
debug('completed! %j', {
75+
cmd: cmd,
76+
bin: bin,
77+
args: args,
78+
opts: opts,
79+
code: code
80+
});
81+
82+
fn(null, Buffer.concat(output).toString('utf-8'));
83+
});
84+
});
85+
}
86+
87+
/**
88+
* Gets the absolute path for a `cmd`.
89+
* @param {String} cmd - e.g. `codesign`.
90+
* @param {Function} fn - Callback which receives `(err, binPath)`.
91+
* @return {void}
92+
*/
93+
function getBinPath(cmd, fn) {
94+
which(cmd, function(err, bin) {
95+
if (err) return fn(err);
96+
97+
fs.exists(bin, function(exists) {
98+
if (!exists) {
99+
return fn(new Error(format(
100+
'Expected file for `%s` does not exist at `%s`',
101+
cmd, bin)));
102+
}
103+
fn(null, bin);
104+
});
105+
});
106+
}
107+
108+
module.exports = run;

0 commit comments

Comments
 (0)