Files
cypress/scripts/binary/build.coffee
Gleb Bahmutov abf4d85869 switch to electron-builder to code sign and notarize Cypress ap… (#6013)
* electron@7.x

* node12.8.1-chrome78-ff70

* Revert "node12.8.1-chrome78-ff70" for now

This reverts commit db2d521994.

* update sendCommand to log on all sendcommands

* promisification in 6.x

* Revert "Revert "node12.8.1-chrome78-ff70" for now"

This reverts commit 57fe764098.

* fix sendcommand

* fix cdp in electron

* fix desktop-gui test

* skip tests that will be fixed by #4973

* bump MAX_ALLOWED_FILE_SIZE :/

* update electron browser spec

* make new dialog code null-proof

* add failing e2e test for issue 5475

* bump electron packager

* add e2e snapshot

* update deprecated electron getters/setters

https://github.com/electron/electron/blob/7-1-x/docs/api/modernization/property-updates.md

* build and test on Mac

* use electron-builder 20.41.0

that adds an option to use hardened Mac OS, which is necessary
for code notarization later.

See https://github.com/electron-userland/electron-builder/releases/tag/v20.41.0
and https://github.com/electron-userland/electron-builder/pull/3858

* electron-builder and pass hardenedRuntime: true

* uncomment build

* upload built binary on mac

* back to 20.41.0, trying after sign hook without success

* use current electron-builder alias instead of build

* retry smoke test on first failure

* testing

* trying to notarize signed app (that does not have node_modules yet)

* env variable names

* copy node_modules ourselves

* build and bundle binary on mac on circle, inject new context

* enable build steps before electron build

* increase mac build timeout

* update build folder on mac

* uncomment actual electron build command

* set linux target to zip

* set zip as target for all platforms

* updated steps

* put notarization hook back

* tweaks for icons

* remove dist electron before code sign

* icons per platform

* make node_modules copy path platform-specific

* fix linux build unpacked folder

* build mac

* fix lint

* test new mac binary against kitchensink

* working on Linux build

* try building entire thing on Linux

* removing correct electron dist folder

* increase zip size limit for now

* add folder rename on Linux from linux-unpacked to Cypress

* print file sizes before zipping

* move linux-unpacked to build dir function

* try deleting second electron file, but code signing probably would not work

* test windows build [build binary]

* ignore tsc errors

* windows build path

* windows [build binary]

* update windows build folder

* increase binary build timeout on Mac

* no need to pass our dist folder

* adding explicit list of additional binaries to code sign on mac

* yarn lock

* uncomment necessary build steps

* electron dir for Linux

* yarn lock again

* back to execa v3

* use execa v4 in packages launcher

* yarn lock again and again

* updated tests that use execa

* print build folder

* add executable name on Linux

* get rid of execa.shell in build scripts

* remove old and commented out code

* need to test building binary on Windows

* throw error from after sign hook if fails

* use execa to zip

* yarn lock

* fix after merge variable

* update test

* add nohoist ffmpeg installer

* patch

* yarn types pass

* yarn lock has binary

Co-authored-by: Zach Bloomquist <github@chary.us>
Co-authored-by: Brian Mann <brian.mann86@gmail.com>
2020-03-24 21:34:52 -04:00

405 lines
12 KiB
CoffeeScript

_ = require("lodash")
fse = require("fs-extra")
os = require("os")
del = require("del")
path = require("path")
cp = require("child_process")
gulp = require("gulp")
chalk = require("chalk")
Promise = require("bluebird")
gulpDebug = require("gulp-debug")
gulpCoffee = require("gulp-coffee")
pluralize = require("pluralize")
vinylPaths = require("vinyl-paths")
coffee = require("@packages/coffee")
execa = require("execa")
electron = require("@packages/electron")
debug = require("debug")("cypress:binary")
R = require("ramda")
la = require("lazy-ass")
check = require("check-more-types")
humanInterval = require("human-interval")
meta = require("./meta")
smoke = require("./smoke")
packages = require("./util/packages")
xvfb = require("../../cli/lib/exec/xvfb")
{ transformRequires } = require('./util/transform-requires')
{ testStaticAssets } = require('./util/testStaticAssets')
performanceTracking = require('../../packages/server/test/support/helpers/performance.js')
rootPackage = require("@packages/root")
fs = Promise.promisifyAll(fse)
logger = (msg, platform) ->
time = new Date()
timeStamp = time.toLocaleTimeString()
console.log(timeStamp, chalk.yellow(msg), chalk.blue(platform))
logBuiltAllPackages = () ->
console.log("built all packages")
# can pass options to better control the build
# for example
# skipClean - do not delete "dist" folder before build
buildCypressApp = (platform, version, options = {}) ->
la(check.unemptyString(version), "missing version to build", version)
distDir = _.partial(meta.distDir, platform)
buildDir = _.partial(meta.buildDir, platform)
buildAppDir = _.partial(meta.buildAppDir, platform)
log = _.partialRight(logger, platform)
testVersion = (folderNameFn) -> () ->
log("#testVersion")
dir = folderNameFn()
la(check.unemptyString(dir), "missing folder for platform", platform)
console.log("testing dist package version")
console.log("by calling: node index.js --version")
console.log("in the folder %s", dir)
execa("node", ["index.js", "--version"], {
cwd: dir
}).then (result) ->
la(check.unemptyString(result.stdout),
'missing output when getting built version', result)
console.log('app in %s', dir)
console.log('built app version', result.stdout)
la(result.stdout == version, "different version reported",
result.stdout, "from input version to build", version)
console.log('✅ using node --version works')
testBuiltStaticAssets = ->
log('#testBuiltStaticAssets')
testStaticAssets(distDir())
canBuildInDocker = ->
platform is "linux" and os.platform() is "darwin"
badPlatformMismatch = ->
console.error("⛔️ cannot build #{platform} from #{os.platform()}")
console.error("⛔️ should use matching platform to build it")
console.error("program arguments")
console.error(process.argv)
checkPlatform = ->
log("#checkPlatform")
if platform is os.platform()
return
console.log("trying to build #{platform} from #{os.platform()}")
if platform is "linux" and os.platform() is "darwin"
console.log("npm run binary-build-linux")
Promise.reject(new Error("Build platform mismatch"))
cleanupPlatform = ->
log("#cleanupPlatform")
if options.skipClean
log("skipClean")
return
cleanup = ->
dir = distDir()
la(check.unemptyString(dir), "empty dist dir", dir, "for platform", platform)
fs.removeAsync(distDir())
cleanup()
.catch(cleanup)
buildPackages = ->
log("#buildPackages")
packages.runAllBuild()
# Promise.resolve()
.then(R.tap(logBuiltAllPackages))
copyPackages = ->
log("#copyPackages")
packages.copyAllToDist(distDir())
transformSymlinkRequires = ->
log("#transformSymlinkRequires")
transformRequires(distDir())
.then (replaceCount) ->
la(replaceCount > 5, 'expected to replace more than 5 symlink requires, but only replaced', replaceCount)
npmInstallPackages = ->
log("#npmInstallPackages")
pathToPackages = distDir("packages", "*")
packages.npmInstallAll(pathToPackages)
createRootPackage = ->
log("#createRootPackage #{platform} #{version}")
fs.outputJsonAsync(distDir("package.json"), {
name: "cypress"
productName: "Cypress",
description: rootPackage.description
version: version
main: "index.js"
scripts: {}
env: "production"
})
.then =>
str = """
process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV || 'production'
require('./packages/server')
"""
fs.outputFileAsync(distDir("index.js"), str)
removeTypeScript = ->
## remove the .ts files in our packages
log("#removeTypeScript")
del([
## include ts files of packages
distDir("**", "*.ts")
## except those in node_modules
"!" + distDir("**", "node_modules", "**", "*.ts")
])
.then (paths) ->
console.log(
"deleted %d TS %s",
paths.length,
pluralize("file", paths.length)
)
console.log(paths)
# we also don't need ".bin" links inside Electron application
# thus we can go through dist/packages/*/node_modules and remove all ".bin" folders
removeBinFolders = ->
log("#removeBinFolders")
searchMask = distDir("packages", "*", "node_modules", ".bin")
console.log("searching for", searchMask)
del([searchMask])
.then (paths) ->
console.log(
"deleted %d .bin %s",
paths.length,
pluralize("folder", paths.length)
)
console.log(paths)
removeCyFolders = ->
log("#removeCyFolders")
searchMask = distDir("packages", "server", ".cy")
console.log("searching", searchMask)
del([searchMask])
.then (paths) ->
console.log(
"deleted %d .cy %s",
paths.length,
pluralize("file", paths.length)
)
console.log(paths)
cleanJs = ->
log("#cleanJs")
packages.runAllCleanJs()
convertCoffeeToJs = ->
log("#convertCoffeeToJs")
## grab everything in src
## convert to js
new Promise (resolve, reject) =>
gulp.src([
## include coffee files of packages
distDir("**", "*.coffee")
## except those in node_modules
"!" + distDir("**", "node_modules", "**", "*.coffee")
], { sourcemaps: true })
.pipe vinylPaths(del)
.pipe(gulpDebug())
.pipe gulpCoffee({
coffee: coffee
})
.pipe gulp.dest(distDir())
.on("end", resolve)
.on("error", reject)
getIconFilename = (platform) ->
filenames = {
darwin: "cypress.icns"
win32: "cypress.ico"
linux: "icon_512x512.png"
}
iconFilename = electron.icons().getPathToIcon(filenames[platform])
console.log("For platform #{platform} using icon #{iconFilename}")
iconFilename
electronPackAndSign = ->
log("#electronPackAndSign")
# See the internal wiki document "Signing Test Runner on MacOS"
# to learn how to get the right Mac certificate for signing and notarizing
# the built Test Runner application
appFolder = distDir()
outputFolder = meta.buildRootDir(platform)
electronVersion = electron.getElectronVersion()
la(check.unemptyString(electronVersion), "missing Electron version to pack", electronVersion)
iconFilename = getIconFilename(platform)
console.log("output folder: #{outputFolder}")
args = [
"--publish=never",
"--c.electronVersion=#{electronVersion}",
"--c.directories.app=#{appFolder}",
"--c.directories.output=#{outputFolder}",
"--c.icon=#{iconFilename}",
# for now we cannot pack source files in asar file
# because electron-builder does not copy nested folders
# from packages/*/node_modules
# see https://github.com/electron-userland/electron-builder/issues/3185
# so we will copy those folders later ourselves
"--c.asar=false"
]
opts = {
stdio: "inherit"
}
console.log("electron-builder arguments:")
console.log(args.join(' '))
execa('electron-builder', args, opts)
removeDevElectronApp = ->
log("#removeDevElectronApp")
# when we copy packages/electron, we get the "dist" folder with
# empty Electron app, symlinked to our server folder
# in production build, we do not need this link, and it
# would not work anyway with code signing
# hint: you can see all symlinks in the build folder
# using "find build/darwin/Cypress.app/ -type l -ls"
console.log("platform", platform)
electronDistFolder = distDir("packages", "electron", "dist")
la(check.unemptyString(electronDistFolder),
"empty electron dist folder for platform", platform)
console.log("Removing unnecessary folder '#{electronDistFolder}'")
fs.removeAsync(electronDistFolder) # .catch(_.noop) why are we ignoring an error here?!
lsDistFolder = ->
log('#lsDistFolder')
buildFolder = buildDir()
console.log("in build folder %s", buildFolder)
execa('ls', ['-la', buildFolder])
.then R.prop("stdout")
.then console.log
runSmokeTests = ->
log("#runSmokeTests")
run = ->
# make sure to use a longer timeout - on Mac the first
# launch of a built application invokes gatekeeper check
# which takes a couple of seconds
executablePath = meta.buildAppExecutable(platform)
smoke.test(executablePath)
if xvfb.isNeeded()
xvfb.start()
.then(run)
.finally(xvfb.stop)
else
run()
verifyAppCanOpen = ->
if (platform != "darwin") then return Promise.resolve()
appFolder = meta.zipDir(platform)
log("#verifyAppCanOpen #{appFolder}")
new Promise (resolve, reject) =>
args = ["-a", "-vvvv", appFolder]
debug("cmd: spctl #{args.join(' ')}")
sp = cp.spawn "spctl", args, {stdio: "inherit"}
sp.on "exit", (code) ->
if code is 0
resolve()
else
reject new Error("Verifying App via GateKeeper failed")
printPackageSizes = ->
appFolder = meta.buildAppDir(platform, "packages")
log("#printPackageSizes #{appFolder}")
if (platform == "win32") then return Promise.resolve()
# "du" - disk usage utility
# -d -1 depth of 1
# -h human readable sizes (K and M)
args = ["-d", "1", appFolder]
parseDiskUsage = (result) ->
lines = result.stdout.split(os.EOL)
# will store {package name: package size}
data = {}
lines.forEach (line) ->
parts = line.split('\t')
packageSize = parseFloat(parts[0])
folder = parts[1]
packageName = path.basename(folder)
if packageName is "packages"
return # root "packages" information
data[packageName] = packageSize
return data
printDiskUsage = (sizes) ->
bySize = R.sortBy(R.prop('1'))
console.log(bySize(R.toPairs(sizes)))
execa("du", args)
.then(parseDiskUsage)
.then(R.tap(printDiskUsage))
.then((sizes) ->
performanceTracking.track('test runner size', sizes)
)
Promise.resolve()
.then(checkPlatform)
.then(cleanupPlatform)
.then(buildPackages)
.then(copyPackages)
.then(npmInstallPackages)
.then(createRootPackage)
.then(convertCoffeeToJs)
.then(removeTypeScript)
.then(cleanJs)
.then(transformSymlinkRequires)
.then(testVersion(distDir))
.then(testBuiltStaticAssets)
.then(removeBinFolders)
.then(removeCyFolders)
.then(removeDevElectronApp)
.then(electronPackAndSign)
.then(lsDistFolder)
.then(testVersion(buildAppDir))
.then(runSmokeTests)
.then(verifyAppCanOpen)
.then(printPackageSizes)
.return({
buildDir: buildDir()
})
module.exports = buildCypressApp