'use strict' const common = require('./common') const copyFilter = require('./copy-filter') const debug = require('debug')('electron-packager') const download = require('./download') const fs = require('fs-extra') const getMetadataFromPackageJSON = require('./infer') const hooks = require('./hooks') const path = require('path') const targets = require('./targets') const unzip = require('./unzip') const { packageUniversalMac } = require('./universal') function debugHostInfo () { debug(common.hostInfo()) } class Packager { constructor (opts) { this.opts = opts this.tempBase = common.baseTempDir(opts) this.useTempDir = opts.tmpdir !== false this.canCreateSymlinks = undefined } async ensureTempDir () { if (this.useTempDir) { await fs.remove(this.tempBase) } else { return Promise.resolve() } } async testSymlink (comboOpts, zipPath) { await fs.mkdirp(this.tempBase) const testPath = await fs.mkdtemp(path.join(this.tempBase, `symlink-test-${comboOpts.platform}-${comboOpts.arch}-`)) const testFile = path.join(testPath, 'test') const testLink = path.join(testPath, 'testlink') try { await fs.outputFile(testFile, '') await fs.symlink(testFile, testLink) this.canCreateSymlinks = true } catch (e) { /* istanbul ignore next */ this.canCreateSymlinks = false } finally { await fs.remove(testPath) } if (this.canCreateSymlinks) { return this.checkOverwrite(comboOpts, zipPath) } /* istanbul ignore next */ return this.skipHostPlatformSansSymlinkSupport(comboOpts) } /* istanbul ignore next */ skipHostPlatformSansSymlinkSupport (comboOpts) { common.info(`Cannot create symlinks (on Windows hosts, it requires admin privileges); skipping ${comboOpts.platform} platform`, this.opts.quiet) return Promise.resolve() } async overwriteAndCreateApp (outDir, comboOpts, zipPath) { debug(`Removing ${outDir} due to setting overwrite: true`) await fs.remove(outDir) return this.createApp(comboOpts, zipPath) } async extractElectronZip (comboOpts, zipPath, buildDir) { debug(`Extracting ${zipPath} to ${buildDir}`) await unzip(zipPath, buildDir) await hooks.promisifyHooks(this.opts.afterExtract, [buildDir, comboOpts.electronVersion, comboOpts.platform, comboOpts.arch]) } async buildDir (platform, arch) { let buildParentDir if (this.useTempDir) { buildParentDir = this.tempBase } else { buildParentDir = this.opts.out || process.cwd() } await fs.mkdirp(buildParentDir) return await fs.mkdtemp(path.resolve(buildParentDir, `${platform}-${arch}-template-`)) } async createApp (comboOpts, zipPath) { const buildDir = await this.buildDir(comboOpts.platform, comboOpts.arch) common.info(`Packaging app for platform ${comboOpts.platform} ${comboOpts.arch} using electron v${comboOpts.electronVersion}`, this.opts.quiet) debug(`Creating ${buildDir}`) await fs.ensureDir(buildDir) await this.extractElectronZip(comboOpts, zipPath, buildDir) const os = require(targets.osModules[comboOpts.platform]) const app = new os.App(comboOpts, buildDir) return app.create() } async checkOverwrite (comboOpts, zipPath) { const finalPath = common.generateFinalPath(comboOpts) if (await fs.pathExists(finalPath)) { if (this.opts.overwrite) { return this.overwriteAndCreateApp(finalPath, comboOpts, zipPath) } else { common.info(`Skipping ${comboOpts.platform} ${comboOpts.arch} (output dir already exists, use --overwrite to force)`, this.opts.quiet) return true } } else { return this.createApp(comboOpts, zipPath) } } async getElectronZipPath (downloadOpts) { if (this.opts.electronZipDir) { if (await fs.pathExists(this.opts.electronZipDir)) { const zipPath = path.resolve( this.opts.electronZipDir, `electron-v${downloadOpts.version}-${downloadOpts.platform}-${downloadOpts.arch}.zip` ) if (!await fs.pathExists(zipPath)) { throw new Error(`The specified Electron ZIP file does not exist: ${zipPath}`) } return zipPath } throw new Error(`The specified Electron ZIP directory does not exist: ${this.opts.electronZipDir}`) } else { return download.downloadElectronZip(downloadOpts) } } async packageForPlatformAndArchWithOpts (comboOpts, downloadOpts) { const zipPath = await this.getElectronZipPath(downloadOpts) if (!this.useTempDir) { return this.createApp(comboOpts, zipPath) } if (common.isPlatformMac(comboOpts.platform)) { /* istanbul ignore else */ if (this.canCreateSymlinks === undefined) { return this.testSymlink(comboOpts, zipPath) } else if (!this.canCreateSymlinks) { return this.skipHostPlatformSansSymlinkSupport(comboOpts) } } return this.checkOverwrite(comboOpts, zipPath) } async packageForPlatformAndArch (downloadOpts) { // Create delegated options object with specific platform and arch, for output directory naming const comboOpts = { ...this.opts, arch: downloadOpts.arch, platform: downloadOpts.platform, electronVersion: downloadOpts.version } if (common.isPlatformMac(comboOpts.platform) && comboOpts.arch === 'universal') { return packageUniversalMac(this.packageForPlatformAndArchWithOpts.bind(this), await this.buildDir(comboOpts.platform, comboOpts.arch), comboOpts, downloadOpts, this.tempBase) } return this.packageForPlatformAndArchWithOpts(comboOpts, downloadOpts) } } async function packageAllSpecifiedCombos (opts, archs, platforms) { const packager = new Packager(opts) await packager.ensureTempDir() return Promise.all(download.createDownloadCombos(opts, platforms, archs).map( downloadOpts => packager.packageForPlatformAndArch(downloadOpts) )) } module.exports = async function packager (opts) { debugHostInfo() if (debug.enabled) debug(`Packager Options: ${JSON.stringify(opts)}`) const archs = targets.validateListFromOptions(opts, 'arch') const platforms = targets.validateListFromOptions(opts, 'platform') if (!Array.isArray(archs)) return Promise.reject(archs) if (!Array.isArray(platforms)) return Promise.reject(platforms) debug(`Target Platforms: ${platforms.join(', ')}`) debug(`Target Architectures: ${archs.join(', ')}`) const packageJSONDir = path.resolve(process.cwd(), opts.dir) || process.cwd() await getMetadataFromPackageJSON(platforms, opts, packageJSONDir) if (opts.name.endsWith(' Helper')) { throw new Error('Application names cannot end in " Helper" due to limitations on macOS') } debug(`Application name: ${opts.name}`) debug(`Target Electron version: ${opts.electronVersion}`) copyFilter.populateIgnoredPaths(opts) await hooks.promisifyHooks(opts.afterFinalizePackageTargets, [targets.createPlatformArchPairs(opts, platforms, archs).map(([platform, arch]) => ({ platform, arch }))]) const appPaths = await packageAllSpecifiedCombos(opts, archs, platforms) // Remove falsy entries (e.g. skipped platforms) return appPaths.filter(appPath => appPath) }