From 2333171de747489a01da1f96bfe24cde21a8dc6c Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Thu, 14 Apr 2016 22:32:36 -0700 Subject: [PATCH] Many packager improvements; Windows signing! (#413) * Many packager improvements; Windows signing! * Windows signing works now! (Certs are on an external USB stick that must be plugged into the build machine during build. We can't do the same for OS X because certs need to exist in the login Keychain to be found.) Fixes #219 * Signing is now optional (so OS X and Windows contributors can run `npm run package` without errors) * zip, dmg, and deb arguments are now passed in as e.g. "--package=dmg" * Print a huge warning when signing is disabled so we're less likely to ship unsigned binaries to users. * Make console.logs during packaging consistent and parallel ("creating..." followed by "created.") * More aggressive signing warnings * Warn when building OS X app on non-OS X platform (because signing will never work on non-OS X platforms) * Warn when building Windows app on non-Windows platform (because signing doesn't work yet on non-Windows platforms) --- README.md | 9 +- bin/package.js | 198 +++++++++++++++++++++++++++++++------------ bin/release-_post.sh | 2 +- bin/warning.txt | 12 +++ package.json | 2 +- 5 files changed, 165 insertions(+), 58 deletions(-) create mode 100644 bin/warning.txt diff --git a/README.md b/README.md index ede28e25..1ea5023f 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,15 @@ $ npm run package To build for one platform: ``` -$ npm run package -- [platform] [package-type] +$ npm run package -- [platform] ``` -Where `[platform]` is `darwin`, `linux`, or `win32` +Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default). -and `[package-type]` is `all` (default), `deb` or `zip` (`linux` platform only) +The following optional arguments are available: + +- `--package=[type]` - Package only one output file. `type` is `deb`, `dmg`, `zip`, or `all` (default) +- `--sign` - Sign the application (OS X, Windows) #### Windows build notes diff --git a/bin/package.js b/bin/package.js index 4b6f33a3..d45cd957 100755 --- a/bin/package.js +++ b/bin/package.js @@ -4,32 +4,55 @@ * Builds app binaries for OS X, Linux, and Windows. */ -var config = require('../config') var cp = require('child_process') var electronPackager = require('electron-packager') var fs = require('fs') +var minimist = require('minimist') var path = require('path') -var pkg = require('../package.json') var rimraf = require('rimraf') +var config = require('../config') +var pkg = require('../package.json') + var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION +/* + * Path to folder with the following files: + * - Windows Authenticode private key and cert (authenticode.p12) + * - Windows Authenticode password file (authenticode.txt) + */ +var CERT_PATH = process.platform === 'win32' + ? 'D:' + : '/Volumes/Certs' + +var argv = minimist(process.argv.slice(2), { + boolean: [ + 'sign' + ], + default: { + package: 'all', + sign: false + }, + string: [ + 'package' + ] +}) + function build () { rimraf.sync(path.join(config.ROOT_PATH, 'dist')) - var platform = process.argv[2] - var packageType = process.argv.length > 3 ? process.argv[3] : 'all' + var platform = argv._[0] if (platform === 'darwin') { buildDarwin(printDone) } else if (platform === 'win32') { buildWin32(printDone) } else if (platform === 'linux') { - buildLinux(packageType, printDone) + buildLinux(printDone) } else { buildDarwin(function (err, buildPath) { printDone(err, buildPath) buildWin32(function (err, buildPath) { printDone(err, buildPath) - buildLinux(packageType, printDone) + buildLinux(printDone) }) }) } @@ -140,13 +163,25 @@ var linux = { // Note: Application icon for Linux is specified via the BrowserWindow `icon` option. } +/* + * Print a large warning when signing is disabled so we are less likely to accidentally + * ship unsigned binaries to users. + */ +process.on('exit', function () { + if (!argv.sign) { + printWarning() + } +}) + build() function buildDarwin (cb) { var plist = require('plist') + console.log('OS X: Packaging electron...') electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { if (err) return cb(err) + console.log('OS X: Packaged electron.') var appPath = path.join(buildPath[0], config.APP_NAME + '.app') var contentsPath = path.join(appPath, 'Contents') @@ -189,11 +224,24 @@ function buildDarwin (cb) { cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`) if (process.platform === 'darwin') { - var appDmg = require('appdmg') + if (argv.sign) { + signApp(function (err) { + if (err) return cb(err) + pack() + }) + } else { + printWarning() + pack() + } + } else { + printWarning() + } + + function signApp (cb) { var sign = require('electron-osx-sign') /* - * Sign the app with Apple Developer ID certificate. We sign the app for 2 reasons: + * Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons: * - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by * the same author as the current version. * - So users will not a see a warning about the app coming from an "Unidentified @@ -211,48 +259,68 @@ function buildDarwin (cb) { verbose: true } + console.log('OS X: Signing app...') sign(signOpts, function (err) { if (err) return cb(err) + console.log('OS X: Signed app.') + cb(null) + }) + } - // Create .zip file (used by the auto-updater) - var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '-darwin.zip') - cp.execSync(`cd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'}`) - console.log('Created OS X .zip file.') + function pack () { + if (argv.package === 'zip' || argv.package === 'all') { + packageZip() + } + if (argv.package === 'dmg' || argv.package === 'all') { + packageDmg() + } + } - var targetPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg') - rimraf.sync(targetPath) + function packageZip () { + // Create .zip file (used by the auto-updater) + var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '-darwin.zip') + console.log('OS X: Creating zip...') + cp.execSync(`cd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'}`) + console.log('OS X: Created zip.') + } - // Create a .dmg (OS X disk image) file, for easy user installation. - var dmgOpts = { - basepath: config.ROOT_PATH, - target: targetPath, - specification: { - title: config.APP_NAME, - icon: config.APP_ICON + '.icns', - background: path.join(config.STATIC_PATH, 'appdmg.png'), - 'icon-size': 128, - contents: [ - { x: 122, y: 240, type: 'file', path: appPath }, - { x: 380, y: 240, type: 'link', path: '/Applications' }, - // Hide hidden icons out of view, for users who have hidden files shown. - // https://github.com/LinusU/node-appdmg/issues/45#issuecomment-153924954 - { x: 50, y: 500, type: 'position', path: '.background' }, - { x: 100, y: 500, type: 'position', path: '.DS_Store' }, - { x: 150, y: 500, type: 'position', path: '.Trashes' }, - { x: 200, y: 500, type: 'position', path: '.VolumeIcon.icns' } - ] - } + function packageDmg () { + var appDmg = require('appdmg') + + var targetPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg') + rimraf.sync(targetPath) + + // Create a .dmg (OS X disk image) file, for easy user installation. + var dmgOpts = { + basepath: config.ROOT_PATH, + target: targetPath, + specification: { + title: config.APP_NAME, + icon: config.APP_ICON + '.icns', + background: path.join(config.STATIC_PATH, 'appdmg.png'), + 'icon-size': 128, + contents: [ + { x: 122, y: 240, type: 'file', path: appPath }, + { x: 380, y: 240, type: 'link', path: '/Applications' }, + // Hide hidden icons out of view, for users who have hidden files shown. + // https://github.com/LinusU/node-appdmg/issues/45#issuecomment-153924954 + { x: 50, y: 500, type: 'position', path: '.background' }, + { x: 100, y: 500, type: 'position', path: '.DS_Store' }, + { x: 150, y: 500, type: 'position', path: '.Trashes' }, + { x: 200, y: 500, type: 'position', path: '.VolumeIcon.icns' } + ] } + } - var dmg = appDmg(dmgOpts) - dmg.on('error', cb) - dmg.on('progress', function (info) { - if (info.type === 'step-begin') console.log(info.title + '...') - }) - dmg.on('finish', function (info) { - console.log('Created OS X disk image (.dmg) file.') - cb(null, buildPath) - }) + console.log('OS X: Creating dmg...') + var dmg = appDmg(dmgOpts) + dmg.on('error', cb) + dmg.on('progress', function (info) { + if (info.type === 'step-begin') console.log(info.title + '...') + }) + dmg.on('finish', function (info) { + console.log('OS X: Created dmg.') + cb(null, buildPath) }) } }) @@ -261,60 +329,79 @@ function buildDarwin (cb) { function buildWin32 (cb) { var installer = require('electron-winstaller') + console.log('Windows: Packaging electron...') electronPackager(Object.assign({}, all, win32), function (err, buildPath) { if (err) return cb(err) + console.log('Windows: Packaged electron.') - console.log('Creating Windows installer...') + var signWithParams + if (process.platform === 'win32') { + if (argv.sign) { + var certificateFile = path.join(CERT_PATH, 'authenticode.p12') + var certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8') + var timestampServer = 'http://timestamp.comodoca.com' + signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256` + } else { + printWarning() + } + } else { + printWarning() + } + + console.log('Windows: Creating installer...') installer.createWindowsInstaller({ appDirectory: buildPath[0], authors: config.APP_TEAM, - // certificateFile: '', // TODO description: config.APP_NAME, exe: config.APP_NAME + '.exe', iconUrl: config.GITHUB_URL_RAW + '/static/' + config.APP_NAME + '.ico', loadingGif: path.join(config.STATIC_PATH, 'loading.gif'), - remoteReleases: config.GITHUB_URL, name: config.APP_NAME, noMsi: true, outputDirectory: path.join(config.ROOT_PATH, 'dist'), productName: config.APP_NAME, + remoteReleases: config.GITHUB_URL, setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe', setupIcon: config.APP_ICON + '.ico', + signWithParams: signWithParams, title: config.APP_NAME, usePackageJson: false, version: pkg.version }).then(function () { - console.log('Created Windows installer.') + console.log('Windows: Created installer.') cb(null, buildPath) }).catch(cb) }) } -function buildLinux (packageType, cb) { +function buildLinux (cb) { var distPath = path.join(config.ROOT_PATH, 'dist') + console.log('Linux: Packaging electron...') electronPackager(Object.assign({}, all, linux), function (err, buildPath) { if (err) return cb(err) + console.log('Linux: Packaged electron.') for (var i = 0; i < buildPath.length; i++) { var filesPath = buildPath[i] var destArch = filesPath.split('-').pop() - if (packageType === 'deb' || packageType === 'all') { + if (argv.package === 'deb' || argv.package === 'all') { packageDeb(filesPath, destArch) } - if (packageType === 'zip' || packageType === 'all') { + if (argv.package === 'zip' || argv.package === 'all') { packageZip(filesPath, destArch) } } }) function packageDeb (filesPath, destArch) { - // Create .deb file for debian based platforms + // Create .deb file for Debian-based platforms var deb = require('nobin-debian-installer')() var destPath = path.join('/opt', pkg.name) + console.log(`Linux: Creating ${destArch} deb...`) deb.pack({ package: pkg, info: { @@ -333,7 +420,7 @@ function buildLinux (packageType, cb) { cwd: filesPath }], function (err) { if (err) return console.error(err.message || err) - console.log('Created Linux ' + destArch + ' .deb file.') + console.log(`Linux: Created ${destArch} deb.`) }) } @@ -341,8 +428,9 @@ function buildLinux (packageType, cb) { // Create .zip file for Linux var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '-linux-' + destArch + '.zip') var appFolderName = path.basename(filesPath) + console.log(`Linux: Creating ${destArch} zip...`) cp.execSync(`cd ${distPath} && zip -r -y ${zipPath} ${appFolderName}`) - console.log('Created Linux ' + destArch + ' .zip file.') + console.log(`Linux: Created ${destArch} zip.`) } } @@ -350,3 +438,7 @@ function printDone (err, buildPath) { if (err) console.error(err.message || err) else console.log('Built ' + buildPath[0]) } + +function printWarning () { + console.log(fs.readFileSync(path.join(__dirname, 'warning.txt'), 'utf8')) +} diff --git a/bin/release-_post.sh b/bin/release-_post.sh index e02f8641..a3be652a 100755 --- a/bin/release-_post.sh +++ b/bin/release-_post.sh @@ -2,7 +2,7 @@ set -e git diff --exit-code -npm run package +npm run package -- --sign git push git push --tags npm publish diff --git a/bin/warning.txt b/bin/warning.txt new file mode 100644 index 00000000..160d1136 --- /dev/null +++ b/bin/warning.txt @@ -0,0 +1,12 @@ + +********************************************************* + _ _ ___ ______ _ _ _____ _ _ _____ + | | | |/ _ \ | ___ \ \ | |_ _| \ | | __ \ + | | | / /_\ \| |_/ / \| | | | | \| | | \/ + | |/\| | _ || /| . ` | | | | . ` | | __ + \ /\ / | | || |\ \| |\ |_| |_| |\ | |_\ \ + \/ \/\_| |_/\_| \_\_| \_/\___/\_| \_/\____/ + + Application is NOT signed. Do not ship this to users! + +********************************************************* diff --git a/package.json b/package.json index 2d78102b..3675a36e 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "electron-packager": "^6.0.2", "electron-winstaller": "feross/windows-installer#build", "gh-release": "^2.0.3", + "minimist": "^1.2.0", "nobin-debian-installer": "^0.0.9", "plist": "^1.2.0", "standard": "^6.0.5" @@ -71,7 +72,6 @@ "scripts": { "clean": "node ./bin/clean.js", "package": "npm install && npm prune && npm dedupe && node ./bin/package.js", - "size": "npm run package -- darwin && du -ch dist/WebTorrent-darwin-x64 | grep total", "start": "electron .", "test": "standard", "update-authors": "./bin/update-authors.sh"