dmg.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.DmgTarget = void 0;
  4. const app_builder_lib_1 = require("app-builder-lib");
  5. const macCodeSign_1 = require("app-builder-lib/out/codeSign/macCodeSign");
  6. const differentialUpdateInfoBuilder_1 = require("app-builder-lib/out/targets/differentialUpdateInfoBuilder");
  7. const appBuilder_1 = require("app-builder-lib/out/util/appBuilder");
  8. const filename_1 = require("app-builder-lib/out/util/filename");
  9. const builder_util_1 = require("builder-util");
  10. const fs_1 = require("builder-util/out/fs");
  11. const fs_extra_1 = require("fs-extra");
  12. const path = require("path");
  13. const dmgLicense_1 = require("./dmgLicense");
  14. const dmgUtil_1 = require("./dmgUtil");
  15. const os_1 = require("os");
  16. class DmgTarget extends app_builder_lib_1.Target {
  17. constructor(packager, outDir) {
  18. super("dmg");
  19. this.packager = packager;
  20. this.outDir = outDir;
  21. this.options = this.packager.config.dmg || Object.create(null);
  22. }
  23. async build(appPath, arch) {
  24. const packager = this.packager;
  25. // tslint:disable-next-line:no-invalid-template-strings
  26. const artifactName = packager.expandArtifactNamePattern(this.options, "dmg", arch, "${productName}-" + (packager.platformSpecificBuildOptions.bundleShortVersion || "${version}") + "-${arch}.${ext}", true, packager.platformSpecificBuildOptions.defaultArch);
  27. const artifactPath = path.join(this.outDir, artifactName);
  28. await packager.info.callArtifactBuildStarted({
  29. targetPresentableName: "DMG",
  30. file: artifactPath,
  31. arch,
  32. });
  33. const volumeName = (0, filename_1.sanitizeFileName)(this.computeVolumeName(arch, this.options.title));
  34. const tempDmg = await createStageDmg(await packager.getTempFile(".dmg"), appPath, volumeName);
  35. const specification = await this.computeDmgOptions();
  36. // https://github.com/electron-userland/electron-builder/issues/2115
  37. const backgroundFile = specification.background == null ? null : await transformBackgroundFileIfNeed(specification.background, packager.info.tempDirManager);
  38. const finalSize = await computeAssetSize(packager.info.cancellationToken, tempDmg, specification, backgroundFile);
  39. const expandingFinalSize = finalSize * 0.1 + finalSize;
  40. await (0, builder_util_1.exec)("hdiutil", ["resize", "-size", expandingFinalSize.toString(), tempDmg]);
  41. const volumePath = path.join("/Volumes", volumeName);
  42. if (await (0, fs_1.exists)(volumePath)) {
  43. builder_util_1.log.debug({ volumePath }, "unmounting previous disk image");
  44. await (0, dmgUtil_1.detach)(volumePath);
  45. }
  46. if (!(await (0, dmgUtil_1.attachAndExecute)(tempDmg, true, () => customizeDmg(volumePath, specification, packager, backgroundFile)))) {
  47. return;
  48. }
  49. // dmg file must not exist otherwise hdiutil failed (https://github.com/electron-userland/electron-builder/issues/1308#issuecomment-282847594), so, -ov must be specified
  50. const args = ["convert", tempDmg, "-ov", "-format", specification.format, "-o", artifactPath];
  51. if (specification.format === "UDZO") {
  52. args.push("-imagekey", `zlib-level=${process.env.ELECTRON_BUILDER_COMPRESSION_LEVEL || "9"}`);
  53. }
  54. await (0, builder_util_1.spawn)("hdiutil", addLogLevel(args));
  55. if (this.options.internetEnabled && parseInt((0, os_1.release)().split(".")[0], 10) < 19) {
  56. await (0, builder_util_1.exec)("hdiutil", addLogLevel(["internet-enable"]).concat(artifactPath));
  57. }
  58. const licenseData = await (0, dmgLicense_1.addLicenseToDmg)(packager, artifactPath);
  59. if (packager.packagerOptions.effectiveOptionComputed != null) {
  60. await packager.packagerOptions.effectiveOptionComputed({ licenseData });
  61. }
  62. if (this.options.sign === true) {
  63. await this.signDmg(artifactPath);
  64. }
  65. const safeArtifactName = packager.computeSafeArtifactName(artifactName, "dmg");
  66. const updateInfo = this.options.writeUpdateInfo === false ? null : await (0, differentialUpdateInfoBuilder_1.createBlockmap)(artifactPath, this, packager, safeArtifactName);
  67. await packager.info.callArtifactBuildCompleted({
  68. file: artifactPath,
  69. safeArtifactName,
  70. target: this,
  71. arch,
  72. packager,
  73. isWriteUpdateInfo: updateInfo != null,
  74. updateInfo,
  75. });
  76. }
  77. async signDmg(artifactPath) {
  78. if (!(0, macCodeSign_1.isSignAllowed)(false)) {
  79. return;
  80. }
  81. const packager = this.packager;
  82. const qualifier = packager.platformSpecificBuildOptions.identity;
  83. // explicitly disabled if set to null
  84. if (qualifier === null) {
  85. // macPackager already somehow handle this situation, so, here just return
  86. return;
  87. }
  88. const keychainFile = (await packager.codeSigningInfo.value).keychainFile;
  89. const certificateType = "Developer ID Application";
  90. let identity = await (0, macCodeSign_1.findIdentity)(certificateType, qualifier, keychainFile);
  91. if (identity == null) {
  92. identity = await (0, macCodeSign_1.findIdentity)("Mac Developer", qualifier, keychainFile);
  93. if (identity == null) {
  94. return;
  95. }
  96. }
  97. const args = ["--sign", identity.hash];
  98. if (keychainFile != null) {
  99. args.push("--keychain", keychainFile);
  100. }
  101. args.push(artifactPath);
  102. await (0, builder_util_1.exec)("codesign", args);
  103. }
  104. computeVolumeName(arch, custom) {
  105. const appInfo = this.packager.appInfo;
  106. const shortVersion = this.packager.platformSpecificBuildOptions.bundleShortVersion || appInfo.version;
  107. const archString = (0, builder_util_1.getArchSuffix)(arch, this.packager.platformSpecificBuildOptions.defaultArch);
  108. if (custom == null) {
  109. return `${appInfo.productFilename} ${shortVersion}${archString}`;
  110. }
  111. return custom
  112. .replace(/\${arch}/g, archString)
  113. .replace(/\${shortVersion}/g, shortVersion)
  114. .replace(/\${version}/g, appInfo.version)
  115. .replace(/\${name}/g, appInfo.name)
  116. .replace(/\${productName}/g, appInfo.productName);
  117. }
  118. // public to test
  119. async computeDmgOptions() {
  120. const packager = this.packager;
  121. const specification = { ...this.options };
  122. if (specification.icon == null && specification.icon !== null) {
  123. specification.icon = await packager.getIconPath();
  124. }
  125. if (specification.icon != null && (0, builder_util_1.isEmptyOrSpaces)(specification.icon)) {
  126. throw new builder_util_1.InvalidConfigurationError("dmg.icon cannot be specified as empty string");
  127. }
  128. const background = specification.background;
  129. if (specification.backgroundColor != null) {
  130. if (background != null) {
  131. throw new builder_util_1.InvalidConfigurationError("Both dmg.backgroundColor and dmg.background are specified — please set the only one");
  132. }
  133. }
  134. else if (background == null) {
  135. specification.background = await (0, dmgUtil_1.computeBackground)(packager);
  136. }
  137. else {
  138. specification.background = path.resolve(packager.info.projectDir, background);
  139. }
  140. if (specification.format == null) {
  141. if (process.env.ELECTRON_BUILDER_COMPRESSION_LEVEL != null) {
  142. ;
  143. specification.format = "UDZO";
  144. }
  145. else if (packager.compression === "store") {
  146. specification.format = "UDRO";
  147. }
  148. else {
  149. specification.format = packager.compression === "maximum" ? "UDBZ" : "UDZO";
  150. }
  151. }
  152. if (specification.contents == null) {
  153. specification.contents = [
  154. {
  155. x: 130,
  156. y: 220,
  157. },
  158. {
  159. x: 410,
  160. y: 220,
  161. type: "link",
  162. path: "/Applications",
  163. },
  164. ];
  165. }
  166. return specification;
  167. }
  168. }
  169. exports.DmgTarget = DmgTarget;
  170. async function createStageDmg(tempDmg, appPath, volumeName) {
  171. //noinspection SpellCheckingInspection
  172. const imageArgs = addLogLevel(["create", "-srcfolder", appPath, "-volname", volumeName, "-anyowners", "-nospotlight", "-format", "UDRW"]);
  173. if (builder_util_1.log.isDebugEnabled) {
  174. imageArgs.push("-debug");
  175. }
  176. let filesystem = ["HFS+", "-fsargs", "-c c=64,a=16,e=16"];
  177. if (process.arch === "arm64") {
  178. // Apple Silicon `hdiutil` dropped support for HFS+, so we force the latest type
  179. // https://github.com/electron-userland/electron-builder/issues/4606
  180. filesystem = ["APFS"];
  181. builder_util_1.log.warn(null, "Detected arm64 process, HFS+ is unavailable. Creating dmg with APFS - supports Mac OSX 10.12+");
  182. }
  183. imageArgs.push("-fs", ...filesystem);
  184. imageArgs.push(tempDmg);
  185. // The reason for retrying up to ten times is that hdiutil create in some cases fail to unmount due to "resource busy".
  186. // https://github.com/electron-userland/electron-builder/issues/5431
  187. await (0, builder_util_1.retry)(() => (0, builder_util_1.spawn)("hdiutil", imageArgs), 5, 1000);
  188. return tempDmg;
  189. }
  190. function addLogLevel(args) {
  191. args.push(process.env.DEBUG_DMG === "true" ? "-verbose" : "-quiet");
  192. return args;
  193. }
  194. async function computeAssetSize(cancellationToken, dmgFile, specification, backgroundFile) {
  195. const asyncTaskManager = new builder_util_1.AsyncTaskManager(cancellationToken);
  196. asyncTaskManager.addTask((0, fs_extra_1.stat)(dmgFile));
  197. if (specification.icon != null) {
  198. asyncTaskManager.addTask((0, fs_1.statOrNull)(specification.icon));
  199. }
  200. if (backgroundFile != null) {
  201. asyncTaskManager.addTask((0, fs_extra_1.stat)(backgroundFile));
  202. }
  203. let result = 32 * 1024;
  204. for (const stat of await asyncTaskManager.awaitTasks()) {
  205. if (stat != null) {
  206. result += stat.size;
  207. }
  208. }
  209. return result;
  210. }
  211. async function customizeDmg(volumePath, specification, packager, backgroundFile) {
  212. const window = specification.window;
  213. const env = {
  214. ...process.env,
  215. volumePath,
  216. appFileName: `${packager.appInfo.productFilename}.app`,
  217. iconSize: specification.iconSize || 80,
  218. iconTextSize: specification.iconTextSize || 12,
  219. PYTHONIOENCODING: "utf8",
  220. };
  221. if (specification.backgroundColor != null || specification.background == null) {
  222. env.backgroundColor = specification.backgroundColor || "#ffffff";
  223. if (window != null) {
  224. env.windowX = (window.x == null ? 100 : window.x).toString();
  225. env.windowY = (window.y == null ? 400 : window.y).toString();
  226. env.windowWidth = (window.width || 540).toString();
  227. env.windowHeight = (window.height || 380).toString();
  228. }
  229. }
  230. else {
  231. delete env.backgroundColor;
  232. }
  233. const args = ["dmg", "--volume", volumePath];
  234. if (specification.icon != null) {
  235. args.push("--icon", (await packager.getResource(specification.icon)));
  236. }
  237. if (backgroundFile != null) {
  238. args.push("--background", backgroundFile);
  239. }
  240. const data = await (0, appBuilder_1.executeAppBuilderAsJson)(args);
  241. if (data.backgroundWidth != null) {
  242. env.windowWidth = window == null ? null : window.width;
  243. env.windowHeight = window == null ? null : window.height;
  244. if (env.windowWidth == null) {
  245. env.windowWidth = data.backgroundWidth.toString();
  246. }
  247. if (env.windowHeight == null) {
  248. env.windowHeight = data.backgroundHeight.toString();
  249. }
  250. if (env.windowX == null) {
  251. env.windowX = 400;
  252. }
  253. if (env.windowY == null) {
  254. env.windowY = Math.round((1440 - env.windowHeight) / 2).toString();
  255. }
  256. }
  257. Object.assign(env, data);
  258. const asyncTaskManager = new builder_util_1.AsyncTaskManager(packager.info.cancellationToken);
  259. env.iconLocations = await computeDmgEntries(specification, volumePath, packager, asyncTaskManager);
  260. await asyncTaskManager.awaitTasks();
  261. const executePython = async (execName) => {
  262. let pythonPath = process.env.PYTHON_PATH;
  263. if (!pythonPath) {
  264. pythonPath = (await (0, builder_util_1.exec)("which", [execName])).trim();
  265. }
  266. await (0, builder_util_1.exec)(pythonPath, [path.join((0, dmgUtil_1.getDmgVendorPath)(), "dmgbuild/core.py")], {
  267. cwd: (0, dmgUtil_1.getDmgVendorPath)(),
  268. env,
  269. });
  270. };
  271. try {
  272. await executePython("python3");
  273. }
  274. catch (error) {
  275. await executePython("python");
  276. }
  277. return packager.packagerOptions.effectiveOptionComputed == null || !(await packager.packagerOptions.effectiveOptionComputed({ volumePath, specification, packager }));
  278. }
  279. async function computeDmgEntries(specification, volumePath, packager, asyncTaskManager) {
  280. let result = "";
  281. for (const c of specification.contents) {
  282. if (c.path != null && c.path.endsWith(".app") && c.type !== "link") {
  283. builder_util_1.log.warn({ path: c.path, reason: "actual path to app will be used instead" }, "do not specify path for application");
  284. }
  285. const entryPath = c.path || `${packager.appInfo.productFilename}.app`;
  286. const entryName = c.name || path.basename(entryPath);
  287. const escapedEntryName = entryName.replace(/['\\]/g, match => `\\${match}`);
  288. if (result.length !== 0) {
  289. result += ",\n";
  290. }
  291. result += `'${escapedEntryName}': (${c.x}, ${c.y})`;
  292. if (c.type === "link") {
  293. asyncTaskManager.addTask((0, builder_util_1.exec)("ln", ["-s", `/${entryPath.startsWith("/") ? entryPath.substring(1) : entryPath}`, `${volumePath}/${entryName}`]));
  294. }
  295. // use c.path instead of entryPath (to be sure that this logic is not applied to .app bundle) https://github.com/electron-userland/electron-builder/issues/2147
  296. else if (!(0, builder_util_1.isEmptyOrSpaces)(c.path) && (c.type === "file" || c.type === "dir")) {
  297. const source = await packager.getResource(c.path);
  298. if (source == null) {
  299. builder_util_1.log.warn({ entryPath, reason: "doesn't exist" }, "skipped DMG item copying");
  300. continue;
  301. }
  302. const destination = `${volumePath}/${entryName}`;
  303. asyncTaskManager.addTask(c.type === "dir" || (await (0, fs_extra_1.stat)(source)).isDirectory() ? (0, fs_1.copyDir)(source, destination) : (0, fs_1.copyFile)(source, destination));
  304. }
  305. }
  306. return result;
  307. }
  308. async function transformBackgroundFileIfNeed(file, tmpDir) {
  309. if (file.endsWith(".tiff") || file.endsWith(".TIFF")) {
  310. return file;
  311. }
  312. const retinaFile = file.replace(/\.([a-z]+)$/, "@2x.$1");
  313. if (await (0, fs_1.exists)(retinaFile)) {
  314. const tiffFile = await tmpDir.getTempFile({ suffix: ".tiff" });
  315. await (0, builder_util_1.exec)("tiffutil", ["-cathidpicheck", file, retinaFile, "-out", tiffFile]);
  316. return tiffFile;
  317. }
  318. return file;
  319. }
  320. //# sourceMappingURL=dmg.js.map