UNPKG

electron-wix-msi

Version:
651 lines 29.4 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 crypto_1 = require("crypto"); const path = require("path"); const spawn_1 = require("./utils/spawn"); const windows_sign_1 = require("@electron/windows-sign"); 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.autoRunTemplate = getTemplate("auto-run", true); this.fileAssociationHeaderTemplate = getTemplate("file-association-header"); this.fileAssociationTemplate = getTemplate("file-association"); this.iconTemplate = getTemplate("icon"); this.wxsFile = ""; this.arch = "x86"; this.files = []; this.specialFiles = []; this.directories = []; this.registry = []; this.components = []; this.appDirectory = path.normalize(options.appDirectory); this.description = options.description; this.exe = options.exe.replace(/\.exe$/, ""); this.exeFilename = this.exe + ".exe"; this.exeFilePath = this.appDirectory.replace(/[/\\]$/, "") + path.sep + this.exeFilename; 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.upgradeCode = options.upgradeCode || (0, crypto_1.randomUUID)(); 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, crypto_1.randomUUID)().toUpperCase(); this.rebootMode = options.rebootMode || "ReallySuppress"; this.installLevel = options.installLevel || 2; this.hasAssociateExtensions = options.associateExtensions !== undefined; this.associateExtensions = options.associateExtensions; 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.autoRun = options.autoRun || 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; } } if (options.windowsSign) { this.windowsSign = options.windowsSign; } else if (options.signWithParams || options.certificateFile || options.certificatePassword) { this.windowsSign = { signWithParams: options.signWithParams, certificateFile: options.certificateFile, certificatePassword: options.certificatePassword, }; } } 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 (this.ui && typeof this.ui === "object") { 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, "<!-- {{Icon}}-->": this.getIcon(), "<!-- {{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"), "<!-- {{AutoRun}} -->": this.autoRun ? this.autoRunTemplate : "{{remove newline}}", "<!-- {{ShortcutProperties}} -->": shortcutProperties .map(({ key, value }) => this.getShortcutProperty(key, value)) .join("\n"), }; const replacements = { "{{ApplicationBinary}}": this.exe, "{{ApplicationDescription}}": this.description, "{{ApplicationName}}": this.name, "{{ApplicationShortcutGuid}}": (0, crypto_1.randomUUID)(), "{{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, crypto_1.randomUUID)(), "{{ConfigurableDirectory}}": enableChooseDirectory ? `ConfigurableDirectory="${ROOTDIR_NAME}"` : "", "{{PackageScope}}": this.defaultInstallMode, "{{InstallPerUser}}": this.defaultInstallMode === "perUser" ? "1" : "0", "{{ProductCode}}": this.productCode, "{{RandomGuid}}": (0, crypto_1.randomUUID)().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* () { if (this.windowsSign) { return (0, windows_sign_1.sign)(Object.assign(Object.assign({}, this.windowsSign), { files: [msiFile] })); } else { debug("Signing not necessary, no windowsSign given"); } }); } getIcon() { let xml = ""; const iconPath = this.icon || this.exeFilePath; if (this.hasAssociateExtensions) { xml = (0, replace_1.replaceInString)(this.iconTemplate, { "<!-- {{I}} -->": " ", "{{IconId}}": "AppIcon.ico", "{{IconSource}}": iconPath, }); } return xml; } getUI() { let xml = ""; if (this.ui) { xml = this.uiTemplate; } if (this.ui && typeof this.ui === "object") { 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, crypto_1.randomUUID)(); const componentId = this.getComponentId(file.path); let extensionAssociation = ""; if (this.hasAssociateExtensions && file.path === this.exeFilePath) { extensionAssociation = this.getExtensionAssociation(indent + 2); } 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, "<!-- {{ExtensionAssociation}} -->": extensionAssociation, }); return { guid, componentId, xml, file, featureAffinity: file.featureAffinity || "main", }; } getExtensionAssociation(indent) { const shortAppName = this.exe.replace(/[^A-Za-z0-9]/g, ""); const xml = (0, replace_1.replaceInString)(this.fileAssociationHeaderTemplate, { "<!-- {{I}} -->": (0, lodash_1.padStart)("", indent), "{{ApplicationDescription}}": this.name, "{{ApplicationBinary}}": this.exe, "{{ApplicationName}}": this.name, "{{ShortAppName}}": shortAppName, }) + this.getExtensionAssociationList(indent, shortAppName).join("\n"); return xml; } getExtensionAssociationList(indent, shortAppName) { if (this.associateExtensions === undefined) { return [""]; } const extList = this.associateExtensions.replace(".", "").split(/[,;]/); return extList.map((ext) => { const xml = (0, replace_1.replaceInString)(this.fileAssociationTemplate, { "<!-- {{I}} -->": (0, lodash_1.padStart)("", indent), "{{ApplicationDescription}}": this.name, "{{ApplicationBinary}}": this.exe, "{{ApplicationName}}": this.name, "{{ShortAppName}}": shortAppName, "{{IconId}}": "AppIcon.ico", "{{ext}}": ext, }); return xml; }); } getRegistryComponent(registry, indent) { const guid = (0, crypto_1.randomUUID)(); 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, crypto_1.randomUUID)()}`; 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); this.exeFilePath = stubExe; 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: "[APPLICATIONROOTDIRECTORY]{{ApplicationBinary}}.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