@mjcctech/meteor-desktop
Version:
Build a Meteor's desktop client with hot code push.
397 lines (357 loc) • 14.5 kB
JavaScript
"use strict";module.export({default:()=>InstallerBuilder});var regeneratorRuntime;module.link('regenerator-runtime/runtime',{default(v){regeneratorRuntime=v}},0);var shell;module.link('shelljs',{default(v){shell=v}},1);var path;module.link('path',{default(v){path=v}},2);var fs;module.link('fs',{default(v){fs=v}},3);var rimraf;module.link('rimraf',{default(v){rimraf=v}},4);var spawn;module.link('cross-spawn',{default(v){spawn=v}},5);var Log;module.link('./log',{default(v){Log=v}},6);var defaultDependencies;module.link('./defaultDependencies',{default(v){defaultDependencies=v}},7);// eslint-disable-next-line no-unused-vars
/**
* Promisfied rimraf.
*
* @param {string} dirPath - path to the dir to be deleted
* @param {number} delay - delay the task by ms
* @returns {Promise<any>}
*/
function removeDir(dirPath, delay = 0) {
return new Promise((resolve, reject) => {
setTimeout(() => {
rimraf(dirPath, {
maxBusyTries: 100
}, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
}, delay);
});
}
/**
* Wrapper for electron-builder.
*/
class InstallerBuilder {
/**
* @param {MeteorDesktop} $ - context
*
* @constructor
*/
constructor($) {
this.log = new Log('electronBuilder');
this.$ = $;
this.firstPass = true;
this.lastRebuild = {};
this.currentContext = null;
this.installerDir = path.join(this.$.env.options.output, this.$.env.paths.installerDir);
this.platforms = [];
}
async init() {
this.builder = await this.$.getDependency('electron-builder', defaultDependencies['electron-builder']);
const appBuilder = await this.$.getDependency('app-builder-lib', defaultDependencies['electron-builder'], false);
this.yarn = require(path.join(appBuilder.path, 'out', 'util', 'yarn'));
this.getGypEnv = this.yarn.getGypEnv;
this.packageDependencies = require(path.join(appBuilder.path, 'out', 'util', 'packageDependencies'));
}
/**
* Prepares the last rebuild object for electron-builder.
*
* @param {string} arch
* @param {string} platform
* @returns {Object}
*/
prepareLastRebuildObject(arch, platform = process.platform) {
const productionDeps = this.packageDependencies
.createLazyProductionDeps(this.$.env.paths.electronApp.root);
this.lastRebuild = {
frameworkInfo: { version: this.$.getElectronVersion(), useCustomDist: true },
platform,
arch,
productionDeps
};
return this.lastRebuild;
}
/**
* Calls npm rebuild from electron-builder.
* @param {string} arch
* @param {string} platform
* @param {boolean} install
* @returns {Promise}
*/
async installOrRebuild(arch, platform = process.platform, install = false) {
this.log.debug(`calling installOrRebuild from electron-builder for arch ${arch}`);
this.prepareLastRebuildObject(arch, platform);
await this.yarn.installOrRebuild(this.$.desktop.getSettings().builderOptions || {},
this.$.env.paths.electronApp.root, this.lastRebuild, install);
}
/**
* Callback invoked before build is made. Ensures that app.asar have the right rebuilt
* node_modules.
*
* @param {Object} context
* @returns {Promise}
*/
beforeBuild(context) {
this.currentContext = Object.assign({}, context);
return new Promise((resolve, reject) => {
const platformMatches = process.platform === context.platform.nodeName;
const rebuild = platformMatches && context.arch !== this.lastRebuild.arch;
if (!platformMatches) {
this.log.warn('skipping dependencies rebuild because platform is different, if you have native ' +
'node modules as your app dependencies you should od the build on the target platform only');
}
if (!rebuild) {
this.moveNodeModulesOut()
.catch(e => reject(e))
.then(() => setTimeout(() => resolve(false), 2000));
// Timeout helps on Windows to clear the file locks.
} else {
// Lets rebuild the node_modules for different arch.
this.installOrRebuild(context.arch, context.platform.nodeName)
.catch(e => reject(e))
.then(() => this.$.electronApp.installLocalNodeModules(context.arch))
.catch(e => reject(e))
.then(() => {
this.$.electronApp.scaffold.createAppRoot();
this.$.electronApp.scaffold.copySkeletonApp();
return this.$.electronApp.packSkeletonToAsar(
[
this.$.env.paths.electronApp.meteorAsar,
this.$.env.paths.electronApp.desktopAsar,
this.$.env.paths.electronApp.extracted
]
);
})
.catch(e => reject(e))
.then(() => this.moveNodeModulesOut())
.catch(e => reject(e))
.then(() => resolve(false));
}
});
}
/**
* Callback to be invoked after packing. Restores node_modules to the .desktop-build.
* @returns {Promise}
*/
afterPack(context) {
this.platforms = this.platforms
.filter(platform => platform !== context.electronPlatformName);
if (this.platforms.length !== 0) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
shell.config.fatal = true;
if (this.$.utils.exists(this.$.env.paths.electronApp.extractedNodeModules)) {
this.log.debug('injecting extracted modules');
shell.cp(
'-Rf',
this.$.env.paths.electronApp.extractedNodeModules,
path.join(this.getPackagedAppPath(context), 'node_modules')
);
}
this.log.debug('moving node_modules back');
// Move node_modules back.
try {
shell.mv(
this.$.env.paths.electronApp.tmpNodeModules,
this.$.env.paths.electronApp.nodeModules
);
} catch (e) {
reject(e);
return;
} finally {
shell.config.reset();
}
if (this.firstPass) {
this.firstPass = false;
}
this.log.debug('node_modules moved back');
this.wait()
.catch(e => reject(e))
.then(() => resolve());
});
}
/**
* This command kills orphaned MSBuild.exe processes.
* Sometime after native node_modules compilation they are still writing some logs,
* prevent node_modules from being deleted.
*/
killMSBuild() {
if (this.currentContext.platform.nodeName !== 'win32') {
return;
}
try {
const out = spawn
.sync(
'wmic',
['process', 'where', 'caption="MSBuild.exe"', 'get', 'processid']
)
.stdout.toString('utf-8')
.split('\n');
const regex = new RegExp(/(\d+)/, 'gm');
// No we will check for those with the matching params.
out.forEach((line) => {
const match = regex.exec(line) || false;
if (match) {
this.log.debug(`killing MSBuild.exe at pid: ${match[1]}`);
spawn.sync('taskkill', ['/pid', match[1], '/f', '/t']);
}
regex.lastIndex = 0;
});
} catch (e) {
this.log.debug('kill MSBuild failed');
}
}
/**
* Returns the path to packaged app.
* @returns {string}
*/
getPackagedAppPath(context = {}) {
if (this.currentContext.platform.nodeName === 'darwin') {
return path.join(
this.installerDir,
'mac',
`${context.packager.appInfo.productFilename}.app`,
'Contents', 'Resources', 'app'
);
}
const platformDir =
`${this.currentContext.platform.nodeName === 'win32' ? 'win' : 'linux'}-${this.currentContext.arch === 'ia32' ? 'ia32-' : ''}unpacked`;
return path.join(
this.installerDir,
platformDir,
'resources', 'app'
);
}
/**
* On Windows it waits for the app.asar in the packed app to be free (no file locks).
* @returns {*}
*/
wait() {
if (this.currentContext.platform.nodeName !== 'win32') {
return Promise.resolve();
}
const appAsarPath = path.join(
this.getPackagedAppPath(),
'app.asar'
);
let retries = 0;
const self = this;
return new Promise((resolve, reject) => {
function check() {
fs.open(appAsarPath, 'r+', (err, fd) => {
retries += 1;
if (err) {
if (err.code !== 'ENOENT') {
self.log.debug(`waiting for app.asar to be readable, ${'code' in err ? `currently reading it returns ${err.code}` : ''}`);
if (retries < 6) {
setTimeout(() => check(), 4000);
} else {
reject(`file is locked: ${appAsarPath}`);
}
} else {
resolve();
}
} else {
fs.closeSync(fd);
resolve();
}
});
}
check();
});
}
/**
* Prepares the target object passed to the electron-builder.
*
* @returns {Map<Platform, Map<Arch, Array<string>>>}
*/
prepareTargets() {
let arch = this.$.env.options.ia32 ? 'ia32' : 'x64';
arch = this.$.env.options.allArchs ? 'all' : arch;
const targets = [];
if (this.$.env.options.win) {
targets.push(this.builder.dependency.Platform.WINDOWS);
}
if (this.$.env.options.linux) {
targets.push(this.builder.dependency.Platform.LINUX);
}
if (this.$.env.options.mac) {
targets.push(this.builder.dependency.Platform.MAC);
}
if (targets.length === 0) {
if (this.$.env.os.isWindows) {
targets.push(this.builder.dependency.Platform.WINDOWS);
} else if (this.$.env.os.isLinux) {
targets.push(this.builder.dependency.Platform.LINUX);
} else {
targets.push(this.builder.dependency.Platform.MAC);
}
}
return this.builder.dependency.createTargets(targets, null, arch);
}
async build() {
const settings = this.$.desktop.getSettings();
if (!('builderOptions' in settings)) {
this.log.error(
'no builderOptions in settings.json, aborting'
);
process.exit(1);
}
const builderOptions = Object.assign({}, settings.builderOptions);
builderOptions.asar = false;
builderOptions.npmRebuild = true;
builderOptions.beforeBuild = this.beforeBuild.bind(this);
builderOptions.afterPack = this.afterPack.bind(this);
builderOptions.electronVersion = this.$.getElectronVersion();
builderOptions.directories = {
app: this.$.env.paths.electronApp.root,
output: path.join(this.$.env.options.output, this.$.env.paths.installerDir)
};
if ('mac' in builderOptions && 'target' in builderOptions.mac) {
if (builderOptions.mac.target.includes('mas')) {
this.platforms = ['darwin', 'mas'];
}
}
try {
this.log.debug('calling build from electron-builder');
await this.builder.dependency.build(Object.assign({
targets: this.prepareTargets(),
config: builderOptions
}, settings.builderCliOptions));
if (this.$.utils.exists(this.$.env.paths.electronApp.extractedNodeModules)) {
shell.rm('-rf', this.$.env.paths.electronApp.extractedNodeModules);
}
} catch (e) {
this.log.error('error while building installer: ', e);
}
}
/**
* Moves node_modules out of the app because while the app will be packaged
* we do not want it to be there.
* @returns {Promise<any>}
*/
moveNodeModulesOut() {
return new Promise((resolve, reject) => {
this.log.debug('moving node_modules out, because we have them already in' +
' app.asar');
this.killMSBuild();
removeDir(this.$.env.paths.electronApp.tmpNodeModules)
.catch(e => reject(e))
.then(() => {
shell.config.fatal = true;
shell.config.verbose = true;
try {
shell.mv(
this.$.env.paths.electronApp.nodeModules,
this.$.env.paths.electronApp.tmpNodeModules
);
shell.config.reset();
return this.wait();
} catch (e) {
shell.config.reset();
return Promise.reject(e);
}
})
.catch(e => reject(e))
.then(() => removeDir(this.$.env.paths.electronApp.nodeModules, 1000))
.catch(e => reject(e))
.then(() => this.wait())
.catch(reject)
.then(resolve);
});
}
}