Mailspring/build/tasks/codesign-task.coffee
2015-12-07 12:48:26 -05:00

133 lines
5 KiB
CoffeeScript

_ = require 'underscore'
path = require 'path'
fs = require 'fs-plus'
# Codesigning is a Mac-only process that requires a valid Apple
# certificate, the private key, and access to the Mac keychain.
#
# We can only codesign from keys in the keychain. At the end of the day we need
# the certificate and private key to exist in the keychain, and these two
# variables to be set:
#
# XCODE_KEYCHAIN - The path to the keychain we're using
# XCODE_KEYCHAIN_PASSWORD - The keychain password
#
# If these variables are already set we don't have to do anything.
#
# If the keychain and password already exists, we'll know by detecting the
# KEYCHAIN_ACCESS environment variable. It is of the form:
#
# /full/keychain/path/login.keychain:password
#
# In the case of Travis, we need to setup a temp keychain from encrypted files
# in the repository. # We'll decrypt and import our certificates, put them in
# a temporary keychain, and use that.
#
# If you want to verify the app was signed you can run the commands:
#
# spctl -a -t exec -vv /path/to/N1.app
#
# Which should return "satisfies its Designated Requirement"
#
# And:
#
# codesign --verify --deep --verbose=2 /path/to/N1.app
#
# Which should return "accepted"
module.exports = (grunt) ->
{spawnP, shouldPublishBuild} = require('./task-helpers')(grunt)
tmpKeychain = "n1-build.keychain"
grunt.registerTask 'codesign', 'Codesign the app', ->
done = @async()
return unless process.platform is 'darwin'
{XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN} = process.env
if XCODE_KEYCHAIN? and XCODE_KEYCHAIN_PASSWORD?
unlockKeychain(XCODE_KEYCHAIN, XCODE_KEYCHAIN_PASSWORD)
.then(signApp)
.then(verifyApp).then(done).catch(grunt.fail.fatal)
else if process.env.KEYCHAIN_ACCESS?
[XCODE_KEYCHAIN, XCODE_KEYCHAIN_PASSWORD] = KEYCHAIN_ACCESS.split(":")
unlockKeychain(XCODE_KEYCHAIN, XCODE_KEYCHAIN_PASSWORD)
.then(signApp)
.then(verifyApp).then(done).catch(grunt.fail.fatal)
else if process.env.TRAVIS
if shouldPublishBuild()
buildTravisKeychain()
.then(_.partial(signApp, tmpKeychain))
.then(verifyApp)
.then(cleanupKeychain)
.then(done).catch(grunt.fail.fatal)
else
return done()
else
grunt.fail.fatal("Can't codesign without keychain or certs")
unlockKeychain = (keychain, keychainPass) ->
args = ['unlock-keychain', '-p', keychainPass, keychain]
spawnP("security", args)
signApp = (keychain) ->
devId = 'Developer ID Application: InboxApp, Inc.'
appPath = grunt.config.get('nylasGruntConfig.shellAppDir')
args = ['--deep', '--force', '--verbose', '--sign', devId]
args.push("--keychain", keychain) if keychain
args.push(appPath)
spawnP("codesign", args)
buildTravisKeychain = ->
crypto = require('crypto')
tmpPass = crypto.randomBytes(32).toString('hex')
{appleCert, nylasCert, nylasPrivateKey, keyPass} = getCertData()
codesignBin = path.join("/", "usr","bin", "codesign")
# Create a custom, temporary keychain
cleanupKeychain()
.then -> spawnP("security", ["create-keychain", '-p', tmpPass, tmpKeychain])
# Make the custom keychain default, so xcodebuild will use it for signing
.then -> spawnP("security", ["default-keychain", "-s", tmpKeychain])
# Unlock the keychain
.then -> unlockKeychain(tmpKeychain, tmpPass)
# Set keychain timeout to 1 hour for long builds
.then -> spawnP("security", ["set-keychain-settings", "-t", "3600", "-l", tmpKeychain])
# Add certificates to keychain and allow codesign to access them
.then -> spawnP("security", ["import", appleCert, "-k", tmpKeychain, "-T", codesignBin])
.then -> spawnP("security", ["import", nylasCert, "-k", tmpKeychain, "-T", codesignBin])
# Load the password for the private key from environment variables
.then -> spawnP("security", ["import", nylasPrivateKey, "-k", tmpKeychain, "-P", keyPass, "-T", codesignBin])
verifyApp = ->
appPath = grunt.config.get('nylasGruntConfig.shellAppDir')
spawnP("codesign", ["--verify", "--deep", "--verbose=2", appPath])
.then -> spawnP("spctl", ["-a", "-t", "exec", "-vv", appPath])
cleanupKeychain = ->
if fs.existsSync(path.join(process.env.HOME, "Library", "Keychains", tmpKeychain))
return spawnP("security", ["delete-keychain", tmpKeychain])
else return Promise.resolve()
getCertData = ->
certs = path.resolve(path.join('build', 'resources', 'certs'))
appleCert = path.join(certs, 'AppleWWDRCA.cer')
nylasCert = path.join(certs, 'mac-nylas-n1.cer')
nylasPrivateKey = path.join(certs, 'mac-nylas-n1.p12')
keyPass = process.env.APPLE_CODESIGN_KEY_PASSWORD
if not keyPass
throw new Error("APPLE_CODESIGN_KEY_PASSWORD must be set")
if not fs.existsSync(appleCert)
throw new Error("#{appleCert} doesn't exist")
if not fs.existsSync(nylasCert)
throw new Error("#{nylasCert} doesn't exist")
if not fs.existsSync(nylasPrivateKey)
throw new Error("#{nylasPrivateKey} doesn't exist")
return {appleCert, nylasCert, nylasPrivateKey, keyPass}