UNPKG

electron-wix-msi

Version:
555 lines 26.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MSICreator = void 0; const fs = require("fs-extra"); const lodash_1 = require("lodash"); const path = require("path"); const uuid_1 = require("uuid"); const spawn_1 = require("./utils/spawn"); const array_to_tree_1 = require("./utils/array-to-tree"); const detect_wix_1 = require("./utils/detect-wix"); const rc_edit_1 = require("./utils/rc-edit"); const replace_1 = require("./utils/replace"); const version_util_1 = require("./utils/version-util"); const walker_1 = require("./utils/walker"); const getTemplate = (name, trimTrailingNewLine = false) => { const content = fs.readFileSync(path.join(__dirname, `../static/${name}.xml`), 'utf-8'); if (trimTrailingNewLine) { return content.replace(/[\r\n]+$/g, ''); } else { return content; } }; const ROOTDIR_NAME = 'APPLICATIONROOTDIRECTORY'; const debug = require('debug')('electron-wix-msi'); class MSICreator { constructor(options) { this.fileComponentTemplate = getTemplate('file-component'); this.registryComponentTemplate = getTemplate('registry-component'); this.permissionTemplate = getTemplate('permission'); this.componentRefTemplate = getTemplate('component-ref'); this.directoryTemplate = getTemplate('directory'); this.directoryNestedInstallTemplate = getTemplate('directory-nested-install'); this.wixTemplate = getTemplate('wix'); this.uiTemplate = getTemplate('ui', true); this.wixVariableTemplate = getTemplate('wix-variable', true); this.updaterTemplate = getTemplate('updater-feature', true); this.updaterPermissions = getTemplate('updater-permissions'); this.autoLaunchTemplate = getTemplate('auto-launch-feature', true); this.shortcutPropertyTemplate = getTemplate('shortcut-property', true); this.wxsFile = ''; this.arch = 'x86'; this.files = []; this.specialFiles = []; this.directories = []; this.registry = []; this.components = []; this.appDirectory = path.normalize(options.appDirectory); this.certificateFile = options.certificateFile; this.certificatePassword = options.certificatePassword; this.description = options.description; this.exe = options.exe.replace(/\.exe$/, ''); this.icon = options.icon; this.extensions = options.extensions || []; this.lightSwitches = options.lightSwitches || []; this.cultures = options.cultures; this.language = options.language || 1033; this.manufacturer = options.manufacturer; this.name = options.name; this.outputDirectory = options.outputDirectory; this.programFilesFolderName = options.programFilesFolderName || options.name; this.nestedFolderName = options.nestedFolderName || ''; this.shortName = options.shortName || options.name; this.shortcutFolderName = options.shortcutFolderName || options.manufacturer; this.shortcutName = options.shortcutName || options.name; this.signWithParams = options.signWithParams; this.upgradeCode = options.upgradeCode || (0, uuid_1.v4)(); this.semanticVersion = options.version; this.windowsCompliantVersion = (0, version_util_1.getWindowsCompliantVersion)(options.version); this.arch = options.arch || 'x86'; this.defaultInstallMode = options.defaultInstallMode || 'perMachine'; this.productCode = (0, uuid_1.v4)().toUpperCase(); this.rebootMode = options.rebootMode || 'ReallySuppress'; this.installLevel = options.installLevel || 2; this.bundled = options.bundled || false; this.appUserModelId = options.appUserModelId || `com.squirrel.${this.shortName}.${this.exe}`.toLowerCase(); this.toastActivatorClsid = options.toastActivatorClsid; this.ui = options.ui !== undefined ? options.ui : false; this.autoUpdate = false; this.autoLaunch = false; this.autoLaunchArgs = []; if (typeof options.features === 'object' && options.features !== null) { this.autoUpdate = options.features.autoUpdate; if (typeof options.features.autoLaunch === 'object' && options.features.autoLaunch !== null) { this.autoLaunch = options.features.autoLaunch.enabled; this.autoLaunchArgs = options.features.autoLaunch.arguments; } else { this.autoLaunch = options.features.autoLaunch; } } } create() { return __awaiter(this, void 0, void 0, function* () { const { files, directories } = yield (0, walker_1.getDirectoryStructure)(this.appDirectory); const registry = this.getRegistryKeys(); const specialFiles = yield this.getSpecialFiles(); this.files = files; this.specialFiles = specialFiles; this.directories = directories; this.registry = registry; this.tree = yield this.getTree(); const { wxsContent, wxsFile } = yield this.createWxs(); this.wxsFile = wxsFile; const supportBinaries = this.specialFiles.filter((f) => f.path.endsWith('.exe')).map((f) => f.path); return { wxsContent, wxsFile, supportBinaries }; }); } compile() { return __awaiter(this, void 0, void 0, function* () { const light = (0, detect_wix_1.hasLight)(); const candle = (0, detect_wix_1.hasCandle)(); if (!light || !light.has || !candle || !candle.has) { console.warn(`It appears that electron-wix-msi cannot find candle.exe or light.exe.`); console.warn(`Please consult the readme at https://github.com/felixrieseberg/electron-wix-msi`); console.warn(`for information on how to install the Wix toolkit, which is required.\n`); throw new Error(`Could not find light.exe or candle.exe`); } else { console.log(`electron-wix-msi: Using light.exe (${light.version}) and candle.exe (${candle.version})`); } if (!this.wxsFile) { throw new Error(`wxsFile not found. Did you run create() yet?`); } const { wixobjFile } = yield this.createWixobj(); const { msiFile } = yield this.createMsi(); yield this.signMSI(msiFile); return { wixobjFile, msiFile }; }); } createWxs() { return __awaiter(this, void 0, void 0, function* () { const target = path.join(this.outputDirectory, `${this.exe}.wxs`); const base = path.basename(this.appDirectory); const directories = yield this.getDirectoryForTree(this.tree, base, 8, this.programFilesFolderName, ROOTDIR_NAME); const componentRefs = yield this.getFeatureComponentRefs('main'); const updaterComponentRefs = yield this.getFeatureComponentRefs('autoUpdate'); const autoLaunchComponentRefs = yield this.getFeatureComponentRefs('autoLaunch'); let enableChooseDirectory = false; if (typeof this.ui === 'object' && this.ui !== 'null') { const { chooseDirectory } = this.ui; enableChooseDirectory = chooseDirectory || false; } const shortcutProperties = [{ key: 'System.AppUserModel.ID', value: this.appUserModelId }]; if (this.toastActivatorClsid) { shortcutProperties.push({ key: 'System.AppUserModel.ToastActivatorCLSID', value: this.toastActivatorClsid.match(/^{.*}$/) ? this.toastActivatorClsid : `{${this.toastActivatorClsid}}` }); } const scaffoldReplacements = { '<!-- {{ComponentRefs}} -->': componentRefs.map(({ xml }) => xml).join('\n'), '<!-- {{Directories}} -->': directories, '<!-- {{UI}} -->': this.getUI(), '<!-- {{AutoUpdatePermissions}} -->': this.autoUpdate ? this.updaterPermissions : '{{remove newline}}', '<!-- {{AutoUpdateFeature}} -->': this.autoUpdate ? this.updaterTemplate : '{{remove newline}}', '<!-- {{AutoLaunchFeature}} -->': this.autoLaunch ? this.autoLaunchTemplate : '{{remove newline}}', '<!-- {{UpdaterComponentRefs}} -->': updaterComponentRefs.map(({ xml }) => xml).join('\n'), '<!-- {{AutoLaunchComponentRefs}} -->': autoLaunchComponentRefs.map(({ xml }) => xml).join('\n'), '<!-- {{ShortcutProperties}} -->': shortcutProperties.map(({ key, value }) => this.getShortcutProperty(key, value)).join('\n'), }; const replacements = { '{{ApplicationBinary}}': this.exe, '{{ApplicationDescription}}': this.description, '{{ApplicationName}}': this.name, '{{ApplicationShortcutGuid}}': (0, uuid_1.v4)(), '{{ApplicationShortName}}': this.shortName, '{{AppUserModelId}}': this.appUserModelId, '{{Language}}': this.language.toString(10), '{{Manufacturer}}': this.manufacturer, '{{ShortcutFolderName}}': this.shortcutFolderName, '{{ShortcutName}}': this.shortcutName, '{{UpgradeCode}}': this.upgradeCode, '{{Version}}': this.windowsCompliantVersion, '{{SemanticVersion}}': this.semanticVersion, '{{Platform}}': this.arch, '{{ProgramFilesFolder}}': this.arch === 'x86' ? 'ProgramFilesFolder' : 'ProgramFiles64Folder', '{{ProcessorArchitecture}}': this.arch, '{{Win64YesNo}}': this.arch === 'x86' ? 'no' : 'yes', '{{DesktopShortcutGuid}}': (0, uuid_1.v4)(), '{{ConfigurableDirectory}}': enableChooseDirectory ? `ConfigurableDirectory="${ROOTDIR_NAME}"` : '', '{{PackageScope}}': this.defaultInstallMode, '{{InstallPerUser}}': this.defaultInstallMode === 'perUser' ? '1' : '0', '{{ProductCode}}': this.productCode, '{{RandomGuid}}': (0, uuid_1.v4)().toString(), '{{RebootMode}}': this.rebootMode, '{{InstallLevel}}': this.installLevel.toString(10), '\r?\n.*{{remove newline}}': '' }; const completeTemplate = (0, replace_1.replaceInString)(this.wixTemplate, scaffoldReplacements); const output = yield (0, replace_1.replaceToFile)(completeTemplate, target, replacements); return { wxsFile: target, wxsContent: output }; }); } createWixobj() { return __awaiter(this, void 0, void 0, function* () { return { wixobjFile: yield this.createFire('wixobj') }; }); } createMsi() { return __awaiter(this, void 0, void 0, function* () { return { msiFile: yield this.createFire('msi') }; }); } createFire(type) { return __awaiter(this, void 0, void 0, function* () { const cwd = path.dirname(this.wxsFile); const expectedObj = path.join(cwd, `${path.basename(this.wxsFile, '.wxs')}.${type}`); const binary = type === 'msi' ? 'light.exe' : 'candle.exe'; const input = type === 'msi' ? path.join(cwd, `${path.basename(this.wxsFile, '.wxs')}.wixobj`) : this.wxsFile; if (this.ui && !this.extensions.find((e) => e === 'WixUIExtension')) { this.extensions.push('WixUIExtension'); } if (!this.extensions.find((e) => e === 'WixUtilExtension')) { this.extensions.push('WixUtilExtension'); } const preArgs = (0, lodash_1.flatMap)(this.extensions.map((e) => (['-ext', e]))); if (typeof this.ui === 'object' && this.ui.localizations && this.ui.localizations.length && type === 'msi') { this.ui.localizations.forEach((l) => preArgs.push('-loc', l)); } if (type === 'msi' && this.cultures) { preArgs.unshift(`-cultures:${this.cultures}`); } if (type === 'msi' && this.lightSwitches) { this.lightSwitches.forEach((param) => preArgs.unshift(param)); } const { code, stderr, stdout } = yield (0, spawn_1.spawnPromise)(binary, [...preArgs, input], { env: process.env, cwd }); if (code === 0 && fs.existsSync(expectedObj)) { return expectedObj; } else { throw new Error(`Could not create ${type} file. Code: ${code} StdErr: ${stderr} StdOut: ${stdout}`); } }); } signMSI(msiFile) { return __awaiter(this, void 0, void 0, function* () { const { certificatePassword, certificateFile, signWithParams } = this; const signToolPath = path.join(__dirname, '../vendor/signtool.exe'); if (!certificateFile && !signWithParams) { debug('Signing not necessary, no certificate file or parameters given'); return; } if (!signWithParams && !certificatePassword) { throw new Error('You must provide a certificatePassword with a certificateFile'); } const args = signWithParams ? signWithParams.match(/(?:[^\s"]+|"[^"]*")+/g) : ['/a', '/f', path.resolve(certificateFile), '/p', certificatePassword]; const { code, stderr, stdout } = yield (0, spawn_1.spawnPromise)(signToolPath, ['sign', ...args, msiFile], { env: process.env, cwd: path.join(__dirname, '../vendor'), }); if (code !== 0) { throw new Error(`Signtool exited with code ${code}. Stderr: ${stderr}. Stdout: ${stdout}`); } }); } getUI() { let xml = ''; if (this.ui) { xml = this.uiTemplate; } if (typeof this.ui === 'object' && this.ui !== 'null') { const { template } = this.ui; const variablesXml = this.getUIVariables(this.ui); const uiTemplate = template || this.uiTemplate; xml = (0, replace_1.replaceInString)(uiTemplate, { '<!-- {{WixVariables}} -->': variablesXml.length > 0 ? variablesXml : '{{remove newline}}' }); } return xml; } getUIVariables(ui) { const images = ui.images || {}; const variableMap = { background: 'WixUIDialogBmp', banner: 'WixUIBannerBmp', exclamationIcon: 'WixUIExclamationIco', infoIcon: 'WixUIInfoIco', newIcon: 'WixUINewIco', upIcon: 'WixUIUpIco' }; return Object.keys(images) .map((key) => { return variableMap[key] ? (0, replace_1.replaceInString)(this.wixVariableTemplate, { '{{Key}}': variableMap[key], '{{Value}}': images[key] }) : ''; }) .join('\n'); } getDirectoryForTree(tree, treePath, indent, name, id) { const childDirectories = Object.keys(tree) .filter((k) => !k.startsWith('__ELECTRON_WIX_MSI')) .map((k) => { return this.getDirectoryForTree(tree[k], tree[k].__ELECTRON_WIX_MSI_PATH__, indent + 2, tree[k].__ELECTRON_WIX_MSI_DIR_NAME__); }); const childFiles = tree.__ELECTRON_WIX_MSI_FILES__ .map((file) => { const component = this.getFileComponent(file, indent + 2); this.components.push(component); return component.xml; }); const childRegistry = tree.__ELECTRON_WIX_MSI_REGISTRY__ .map((registry) => { const component = this.getRegistryComponent(registry, indent + 2); this.components.push(component); return component.xml; }); const children = [childDirectories.join('\n'), childFiles.join('\n'), childRegistry.length > 0 ? '\n' : '', childRegistry.join('\n')].join(''); let directoryXml; if (this.nestedFolderName && indent === 8) { directoryXml = (0, replace_1.replaceInString)(this.directoryNestedInstallTemplate, { '<!-- {{I}} -->': (0, lodash_1.padStart)('', indent), '{{DirectoryId}}': id || this.getComponentId(treePath), '{{DirectoryName}}': name, '{{NestedDirectoryName}}': this.nestedFolderName, '<!-- {{Children}} -->': children }); } else { directoryXml = (0, replace_1.replaceInString)(this.directoryTemplate, { '<!-- {{I}} -->': (0, lodash_1.padStart)('', indent), '{{DirectoryId}}': id || this.getComponentId(treePath), '{{DirectoryName}}': name, '<!-- {{Children}} -->': children }); } return `${directoryXml}${childDirectories.length > 0 && !id ? '\n' : ''}`; } getTree() { return __awaiter(this, void 0, void 0, function* () { const root = this.appDirectory; const folderTree = (0, array_to_tree_1.arrayToTree)(this.directories, root, this.semanticVersion); const fileFolderTree = (0, array_to_tree_1.addFilesToTree)(folderTree, this.files, this.specialFiles, this.registry, this.semanticVersion); return fileFolderTree; }); } getFeatureComponentRefs(feature) { return this.components .filter((c) => c.featureAffinity === feature) .map(({ componentId }) => { const xml = (0, replace_1.replaceInString)(this.componentRefTemplate, { '<!-- {{I}} -->': ' ', '{{ComponentId}}': componentId }); return { componentId, xml }; }); } getFileComponent(file, indent) { const guid = (0, uuid_1.v4)(); const componentId = this.getComponentId(file.path); const xml = (0, replace_1.replaceInString)(this.fileComponentTemplate, { '<!-- {{I}} -->': (0, lodash_1.padStart)('', indent), '{{ComponentId}}': componentId, '{{FileId}}': componentId, '{{Name}}': file.name, '{{Guid}}': guid, '{{SourcePath}}': file.path }); return { guid, componentId, xml, file, featureAffinity: file.featureAffinity || 'main' }; } getRegistryComponent(registry, indent) { const guid = (0, uuid_1.v4)(); const permissionXml = registry.permission ? (0, replace_1.replaceInString)(this.permissionTemplate, { '{{User}}': registry.permission.user, '{{GenericAll}}': registry.permission.genericAll, }) : '{{remove newline}}'; const xml = (0, replace_1.replaceInString)(this.registryComponentTemplate, { '<!-- {{I}} -->': (0, lodash_1.padStart)('', indent), '{{ComponentId}}': registry.id, '{{Guid}}': guid, '{{Name}}': registry.name, '{{Root}}': registry.root, '{{Key}}': registry.key, '{{Type}}': registry.type, '{{Value}}': registry.value, '{{ForceCreateOnInstall}}': registry.forceCreateOnInstall || 'no', '{{ForceDeleteOnUninstall}}': registry.forceDeleteOnUninstall || 'no', '<!-- {{Permission}} -->': permissionXml }); return { guid, componentId: registry.id, xml, featureAffinity: registry.featureAffinity || 'main' }; } getShortcutProperty(key, value) { const xml = (0, replace_1.replaceInString)(this.shortcutPropertyTemplate, { '{{ShortcutPropertyKey}}': key, '{{ShortcutPropertyValue}}': value, }); return xml; } getComponentId(filePath) { const pathId = filePath .replace(this.appDirectory, '') .replace(/^\\|\//g, ''); const pathPart = pathId.length > 34 ? path.basename(filePath).slice(0, 34) : pathId; const uniqueId = `_${pathPart}_${(0, uuid_1.v4)()}`; return uniqueId.replace(/[^A-Za-z0-9_\.]/g, '_'); } getSpecialFiles() { return __awaiter(this, void 0, void 0, function* () { const specialFiles = new Array(); const stubExe = yield (0, rc_edit_1.createStubExe)(this.appDirectory, this.exe, this.shortName, this.manufacturer, this.description, this.windowsCompliantVersion, this.icon); const installInfoFile = (0, version_util_1.createInstallInfoFile)(this.manufacturer, this.shortName, this.productCode, this.semanticVersion, this.arch); specialFiles.push({ name: `${this.exe}.exe`, path: stubExe }); specialFiles.push({ name: `.installInfo.json`, path: installInfoFile }); if (this.autoUpdate) { specialFiles.push({ name: `Update.exe`, path: path.join(__dirname, '../vendor/msq.exe'), featureAffinity: 'autoUpdate' }); } return specialFiles; }); } getRegistryKeys() { const registry = new Array(); const uninstallKey = 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{{{ProductCode}}}.msq'; const productRegKey = 'SOFTWARE\\{{Manufacturer}}\\{{ApplicationShortName}}'; registry.push({ id: 'RegistryInstallPath', root: 'HKMU', name: 'InstallPath', key: uninstallKey, type: 'string', value: '[APPLICATIONROOTDIRECTORY]', forceDeleteOnUninstall: 'yes' }); if (!this.bundled) { registry.push({ id: 'UninstallDisplayName', root: 'HKMU', name: 'DisplayName', key: uninstallKey, type: 'string', value: '[VisibleProductName]', forceDeleteOnUninstall: 'yes' }); } registry.push({ id: 'UninstallPublisher', root: 'HKMU', name: 'Publisher', key: uninstallKey, type: 'string', value: '{{Manufacturer}}', forceDeleteOnUninstall: 'yes' }); registry.push({ id: 'UninstallDisplayVersion', root: 'HKMU', name: 'DisplayVersion', key: uninstallKey, type: 'string', value: '{{SemanticVersion}}', forceDeleteOnUninstall: 'yes' }); registry.push({ id: 'UninstallModifyString', root: 'HKMU', name: 'ModifyPath', key: uninstallKey, type: 'expandable', value: 'MsiExec.exe /I {{{ProductCode}}}', forceDeleteOnUninstall: 'yes' }); registry.push({ id: 'UninstallString', root: 'HKMU', name: 'UninstallString', key: uninstallKey, type: 'expandable', value: 'MsiExec.exe /X {{{ProductCode}}}', forceDeleteOnUninstall: 'yes' }); registry.push({ id: 'UninstallDisplayIcon', root: 'HKMU', name: 'DisplayIcon', key: uninstallKey, type: 'expandable', value: this.arch === 'x86' ? '[SystemFolder]msiexec.exe' : '[System64Folder]msiexec.exe', forceDeleteOnUninstall: 'yes' }); if (this.autoUpdate) { registry.push({ id: 'SetUninstallDisplayVersionPermissions', root: 'HKMU', name: 'DisplayVersion', key: uninstallKey, type: 'string', value: '{{SemanticVersion}}', featureAffinity: 'autoUpdate', permission: { user: '[UPDATERUSERGROUP]', genericAll: 'yes' }, forceCreateOnInstall: 'yes', }); registry.push({ id: 'AutoUpdateEnabled', root: 'HKMU', name: 'AutoUpdate', key: productRegKey, type: 'integer', value: '[AUTOUPDATEENABLED]', featureAffinity: 'autoUpdate', forceDeleteOnUninstall: 'yes' }); } if (this.autoLaunch) { const args = this.autoLaunchArgs.length > 0 ? ` ${this.autoLaunchArgs.join(' ')}`.replace(/"/gi, '&quot;') : ''; registry.push({ id: 'RegistryRunKey', root: 'HKMU', name: '{{AppUserModelId}}', key: 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run', type: 'string', value: `&quot;[APPLICATIONROOTDIRECTORY]{{ApplicationBinary}}.exe&quot;${args}`, featureAffinity: 'autoLaunch', forceDeleteOnUninstall: 'no' }); } return registry; } } exports.MSICreator = MSICreator; //# sourceMappingURL=creator.js.map