MsiTarget.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const bluebird_lst_1 = require("bluebird-lst");
  4. const builder_util_1 = require("builder-util");
  5. const builder_util_runtime_1 = require("builder-util-runtime");
  6. const binDownload_1 = require("../binDownload");
  7. const fs_1 = require("builder-util/out/fs");
  8. const crypto_1 = require("crypto");
  9. const ejs = require("ejs");
  10. const promises_1 = require("fs/promises");
  11. const lazy_val_1 = require("lazy-val");
  12. const path = require("path");
  13. const core_1 = require("../core");
  14. const CommonWindowsInstallerConfiguration_1 = require("../options/CommonWindowsInstallerConfiguration");
  15. const platformPackager_1 = require("../platformPackager");
  16. const pathManager_1 = require("../util/pathManager");
  17. const vm_1 = require("../vm/vm");
  18. const WineVm_1 = require("../vm/WineVm");
  19. const targetUtil_1 = require("./targetUtil");
  20. const ELECTRON_BUILDER_UPGRADE_CODE_NS_UUID = builder_util_runtime_1.UUID.parse("d752fe43-5d44-44d5-9fc9-6dd1bf19d5cc");
  21. const ROOT_DIR_ID = "APPLICATIONFOLDER";
  22. // WiX doesn't support Mono, so, dontnet462 is required to be installed for wine (preinstalled in our bundled wine)
  23. class MsiTarget extends core_1.Target {
  24. constructor(packager, outDir, name = "msi", isAsyncSupported = true) {
  25. super(name, isAsyncSupported);
  26. this.packager = packager;
  27. this.outDir = outDir;
  28. this.vm = process.platform === "win32" ? new vm_1.VmManager() : new WineVm_1.WineVmManager();
  29. this.options = (0, builder_util_1.deepAssign)(this.packager.platformSpecificBuildOptions, this.packager.config.msi);
  30. this.projectTemplate = new lazy_val_1.Lazy(async () => {
  31. const template = (await (0, promises_1.readFile)(path.join((0, pathManager_1.getTemplatePath)(this.name), "template.xml"), "utf8"))
  32. .replace(/{{/g, "<%")
  33. .replace(/}}/g, "%>")
  34. .replace(/\${([^}]+)}/g, "<%=$1%>");
  35. return ejs.compile(template);
  36. });
  37. }
  38. /**
  39. * A product-specific string that can be used in an [MSI Identifier](https://docs.microsoft.com/en-us/windows/win32/msi/identifier).
  40. */
  41. get productMsiIdPrefix() {
  42. const sanitizedId = this.packager.appInfo.productFilename.replace(/[^\w.]/g, "").replace(/^[^A-Za-z_]+/, "");
  43. return sanitizedId.length > 0 ? sanitizedId : "App" + this.upgradeCode.replace(/-/g, "");
  44. }
  45. get iconId() {
  46. return `${this.productMsiIdPrefix}Icon.exe`;
  47. }
  48. get upgradeCode() {
  49. return (this.options.upgradeCode || builder_util_runtime_1.UUID.v5(this.packager.appInfo.id, ELECTRON_BUILDER_UPGRADE_CODE_NS_UUID)).toUpperCase();
  50. }
  51. async build(appOutDir, arch) {
  52. const packager = this.packager;
  53. const artifactName = packager.expandArtifactBeautyNamePattern(this.options, "msi", arch);
  54. const artifactPath = path.join(this.outDir, artifactName);
  55. await packager.info.callArtifactBuildStarted({
  56. targetPresentableName: "MSI",
  57. file: artifactPath,
  58. arch,
  59. });
  60. const stageDir = await (0, targetUtil_1.createStageDir)(this, packager, arch);
  61. const vm = this.vm;
  62. const commonOptions = (0, CommonWindowsInstallerConfiguration_1.getEffectiveOptions)(this.options, this.packager);
  63. const projectFile = stageDir.getTempFile("project.wxs");
  64. const objectFiles = ["project.wixobj"];
  65. await (0, promises_1.writeFile)(projectFile, await this.writeManifest(appOutDir, arch, commonOptions));
  66. await packager.info.callMsiProjectCreated(projectFile);
  67. // noinspection SpellCheckingInspection
  68. const vendorPath = await (0, binDownload_1.getBinFromUrl)("wix", "4.0.0.5512.2", "/X5poahdCc3199Vt6AP7gluTlT1nxi9cbbHhZhCMEu+ngyP1LiBMn+oZX7QAZVaKeBMc2SjVp7fJqNLqsUnPNQ==");
  69. // noinspection SpellCheckingInspection
  70. const candleArgs = ["-arch", arch === builder_util_1.Arch.ia32 ? "x86" : arch === builder_util_1.Arch.arm64 ? "arm64" : "x64", `-dappDir=${vm.toVmFile(appOutDir)}`].concat(this.getCommonWixArgs());
  71. candleArgs.push("project.wxs");
  72. await vm.exec(vm.toVmFile(path.join(vendorPath, "candle.exe")), candleArgs, {
  73. cwd: stageDir.dir,
  74. });
  75. await this.light(objectFiles, vm, artifactPath, appOutDir, vendorPath, stageDir.dir);
  76. await stageDir.cleanup();
  77. await packager.sign(artifactPath);
  78. await packager.info.callArtifactBuildCompleted({
  79. file: artifactPath,
  80. packager,
  81. arch,
  82. safeArtifactName: packager.computeSafeArtifactName(artifactName, "msi"),
  83. target: this,
  84. isWriteUpdateInfo: false,
  85. });
  86. }
  87. async light(objectFiles, vm, artifactPath, appOutDir, vendorPath, tempDir) {
  88. // noinspection SpellCheckingInspection
  89. const lightArgs = [
  90. "-out",
  91. vm.toVmFile(artifactPath),
  92. "-v",
  93. // https://github.com/wixtoolset/issues/issues/5169
  94. "-spdb",
  95. // https://sourceforge.net/p/wix/bugs/2405/
  96. // error LGHT1076 : ICE61: This product should remove only older versions of itself. The Maximum version is not less than the current product. (1.1.0.42 1.1.0.42)
  97. "-sw1076",
  98. `-dappDir=${vm.toVmFile(appOutDir)}`,
  99. // "-dcl:high",
  100. ].concat(this.getCommonWixArgs());
  101. // http://windows-installer-xml-wix-toolset.687559.n2.nabble.com/Build-3-5-2229-0-give-me-the-following-error-error-LGHT0216-An-unexpected-Win32-exception-with-errorn-td5707443.html
  102. if (process.platform !== "win32") {
  103. // noinspection SpellCheckingInspection
  104. lightArgs.push("-sval");
  105. }
  106. if (this.options.oneClick === false) {
  107. lightArgs.push("-ext", "WixUIExtension");
  108. }
  109. // objectFiles - only filenames, we set current directory to our temp stage dir
  110. lightArgs.push(...objectFiles);
  111. await vm.exec(vm.toVmFile(path.join(vendorPath, "light.exe")), lightArgs, {
  112. cwd: tempDir,
  113. });
  114. }
  115. getCommonWixArgs() {
  116. const args = ["-pedantic"];
  117. if (this.options.warningsAsErrors !== false) {
  118. args.push("-wx");
  119. }
  120. if (this.options.additionalWixArgs != null) {
  121. args.push(...this.options.additionalWixArgs);
  122. }
  123. return args;
  124. }
  125. async writeManifest(appOutDir, arch, commonOptions) {
  126. const appInfo = this.packager.appInfo;
  127. const { files, dirs } = await this.computeFileDeclaration(appOutDir);
  128. const options = this.options;
  129. return (await this.projectTemplate.value)({
  130. ...(await this.getBaseOptions(commonOptions)),
  131. isCreateDesktopShortcut: commonOptions.isCreateDesktopShortcut !== CommonWindowsInstallerConfiguration_1.DesktopShortcutCreationPolicy.NEVER,
  132. isRunAfterFinish: options.runAfterFinish !== false,
  133. // https://stackoverflow.com/questions/1929038/compilation-error-ice80-the-64bitcomponent-uses-32bitdirectory
  134. programFilesId: arch === builder_util_1.Arch.x64 ? "ProgramFiles64Folder" : "ProgramFilesFolder",
  135. // wix in the name because special wix format can be used in the name
  136. installationDirectoryWixName: (0, targetUtil_1.getWindowsInstallationDirName)(appInfo, commonOptions.isAssisted || commonOptions.isPerMachine === true),
  137. dirs,
  138. files,
  139. });
  140. }
  141. async getBaseOptions(commonOptions) {
  142. const appInfo = this.packager.appInfo;
  143. const iconPath = await this.packager.getIconPath();
  144. const compression = this.packager.compression;
  145. const companyName = appInfo.companyName;
  146. if (!companyName) {
  147. builder_util_1.log.warn(`Manufacturer is not set for MSI — please set "author" in the package.json`);
  148. }
  149. return {
  150. ...commonOptions,
  151. iconPath: iconPath == null ? null : this.vm.toVmFile(iconPath),
  152. iconId: this.iconId,
  153. compressionLevel: compression === "store" ? "none" : "high",
  154. version: appInfo.getVersionInWeirdWindowsForm(),
  155. productName: appInfo.productName,
  156. upgradeCode: this.upgradeCode,
  157. manufacturer: companyName || appInfo.productName,
  158. appDescription: appInfo.description,
  159. };
  160. }
  161. async computeFileDeclaration(appOutDir) {
  162. const appInfo = this.packager.appInfo;
  163. let isRootDirAddedToRemoveTable = false;
  164. const dirNames = new Set();
  165. const dirs = [];
  166. const fileSpace = " ".repeat(6);
  167. const commonOptions = (0, CommonWindowsInstallerConfiguration_1.getEffectiveOptions)(this.options, this.packager);
  168. const files = await bluebird_lst_1.default.map((0, fs_1.walk)(appOutDir), file => {
  169. const packagePath = file.substring(appOutDir.length + 1);
  170. const lastSlash = packagePath.lastIndexOf(path.sep);
  171. const fileName = lastSlash > 0 ? packagePath.substring(lastSlash + 1) : packagePath;
  172. let directoryId = null;
  173. let dirName = "";
  174. // Wix Directory.FileSource doesn't work - https://stackoverflow.com/questions/21519388/wix-filesource-confusion
  175. if (lastSlash > 0) {
  176. // This Name attribute may also define multiple directories using the inline directory syntax.
  177. // For example, "ProgramFilesFolder:\My Company\My Product\bin" would create a reference to a Directory element with Id="ProgramFilesFolder" then create directories named "My Company" then "My Product" then "bin" nested beneath each other.
  178. // This syntax is a shortcut to defining each directory in an individual Directory element.
  179. dirName = packagePath.substring(0, lastSlash);
  180. // https://github.com/electron-userland/electron-builder/issues/3027
  181. directoryId = "d" + (0, crypto_1.createHash)("md5").update(dirName).digest("base64").replace(/\//g, "_").replace(/\+/g, ".").replace(/=+$/, "");
  182. if (!dirNames.has(dirName)) {
  183. dirNames.add(dirName);
  184. dirs.push(`<Directory Id="${directoryId}" Name="${ROOT_DIR_ID}:\\${dirName.replace(/\//g, "\\")}\\"/>`);
  185. }
  186. }
  187. else if (!isRootDirAddedToRemoveTable) {
  188. isRootDirAddedToRemoveTable = true;
  189. }
  190. // since RegistryValue can be part of Component, *** *** *** *** *** *** *** *** *** wix cannot auto generate guid
  191. // https://stackoverflow.com/questions/1405100/change-my-component-guid-in-wix
  192. let result = `<Component${directoryId === null ? "" : ` Directory="${directoryId}"`}>`;
  193. result += `\n${fileSpace} <File Name="${xmlAttr(fileName)}" Source="$(var.appDir)${path.sep}${xmlAttr(packagePath)}" ReadOnly="yes" KeyPath="yes"`;
  194. const isMainExecutable = packagePath === `${appInfo.productFilename}.exe`;
  195. if (isMainExecutable) {
  196. result += ' Id="mainExecutable"';
  197. }
  198. else if (directoryId === null) {
  199. result += ` Id="${path.basename(packagePath)}_f"`;
  200. }
  201. const isCreateDesktopShortcut = commonOptions.isCreateDesktopShortcut !== CommonWindowsInstallerConfiguration_1.DesktopShortcutCreationPolicy.NEVER;
  202. if (isMainExecutable && (isCreateDesktopShortcut || commonOptions.isCreateStartMenuShortcut)) {
  203. result += `>\n`;
  204. const shortcutName = commonOptions.shortcutName;
  205. if (isCreateDesktopShortcut) {
  206. result += `${fileSpace} <Shortcut Id="desktopShortcut" Directory="DesktopFolder" Name="${xmlAttr(shortcutName)}" WorkingDirectory="APPLICATIONFOLDER" Advertise="yes" Icon="${this.iconId}"/>\n`;
  207. }
  208. const hasMenuCategory = commonOptions.menuCategory != null;
  209. const startMenuShortcutDirectoryId = hasMenuCategory ? "AppProgramMenuDir" : "ProgramMenuFolder";
  210. if (commonOptions.isCreateStartMenuShortcut) {
  211. if (hasMenuCategory) {
  212. dirs.push(`<Directory Id="${startMenuShortcutDirectoryId}" Name="ProgramMenuFolder:\\${commonOptions.menuCategory}\\"/>`);
  213. }
  214. result += `${fileSpace} <Shortcut Id="startMenuShortcut" Directory="${startMenuShortcutDirectoryId}" Name="${xmlAttr(shortcutName)}" WorkingDirectory="APPLICATIONFOLDER" Advertise="yes" Icon="${this.iconId}">\n`;
  215. result += `${fileSpace} <ShortcutProperty Key="System.AppUserModel.ID" Value="${xmlAttr(this.packager.appInfo.id)}"/>\n`;
  216. result += `${fileSpace} </Shortcut>\n`;
  217. }
  218. result += `${fileSpace}</File>`;
  219. if (hasMenuCategory) {
  220. result += `<RemoveFolder Id="${startMenuShortcutDirectoryId}" Directory="${startMenuShortcutDirectoryId}" On="uninstall"/>\n`;
  221. }
  222. }
  223. else {
  224. result += `/>`;
  225. }
  226. const fileAssociations = this.packager.fileAssociations;
  227. if (isMainExecutable && fileAssociations.length !== 0) {
  228. for (const item of fileAssociations) {
  229. const extensions = (0, builder_util_1.asArray)(item.ext).map(platformPackager_1.normalizeExt);
  230. for (const ext of extensions) {
  231. result += `${fileSpace} <ProgId Id="${this.productMsiIdPrefix}.${ext}" Advertise="yes" Icon="${this.iconId}" ${item.description ? `Description="${item.description}"` : ""}>\n`;
  232. result += `${fileSpace} <Extension Id="${ext}" Advertise="yes">\n`;
  233. result += `${fileSpace} <Verb Id="open" Command="Open with ${xmlAttr(this.packager.appInfo.productName)}" Argument="&quot;%1&quot;"/>\n`;
  234. result += `${fileSpace} </Extension>\n`;
  235. result += `${fileSpace} </ProgId>\n`;
  236. }
  237. }
  238. }
  239. return `${result}\n${fileSpace}</Component>`;
  240. });
  241. return { dirs: listToString(dirs, 2), files: listToString(files, 3) };
  242. }
  243. }
  244. exports.default = MsiTarget;
  245. function listToString(list, indentLevel) {
  246. const space = " ".repeat(indentLevel * 2);
  247. return list.join(`\n${space}`);
  248. }
  249. function xmlAttr(str) {
  250. return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
  251. }
  252. //# sourceMappingURL=MsiTarget.js.map