cross-packer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +151 -0
  3. package/README.zh-CN.md +151 -0
  4. package/dist/archive/ar.d.ts +12 -0
  5. package/dist/archive/seven-za.d.ts +32 -0
  6. package/dist/archive/tar.d.ts +30 -0
  7. package/dist/archive/zip-reader.d.ts +17 -0
  8. package/dist/asar/dependency/collect.d.ts +2 -0
  9. package/dist/asar/dependency/detector.d.ts +8 -0
  10. package/dist/asar/dependency/hoister.d.ts +27 -0
  11. package/dist/asar/dependency/utils.d.ts +59 -0
  12. package/dist/asar/fileset/matcher.d.ts +12 -0
  13. package/dist/asar/fileset/platform-fileset.d.ts +36 -0
  14. package/dist/asar/fileset/transformer.d.ts +2 -0
  15. package/dist/asar/fileset/unpack-detector.d.ts +10 -0
  16. package/dist/asar/fileset/walker.d.ts +14 -0
  17. package/dist/asar/pack.d.ts +23 -0
  18. package/dist/asar/packer/app-collector.d.ts +2 -0
  19. package/dist/asar/packer/collector.d.ts +18 -0
  20. package/dist/asar/packer/concurrency.d.ts +1 -0
  21. package/dist/asar/packer/integrity.d.ts +8 -0
  22. package/dist/asar/packer/nm-collector.d.ts +7 -0
  23. package/dist/asar/packer/stream-builder.d.ts +18 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/config/index.d.ts +97 -0
  26. package/dist/electron/dist-zip.d.ts +18 -0
  27. package/dist/electron/download.d.ts +21 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/packer/common/app-update-yml.d.ts +5 -0
  30. package/dist/packer/common/extra-resources.d.ts +39 -0
  31. package/dist/packer/common/staging.d.ts +3 -0
  32. package/dist/packer/common/template.d.ts +2 -0
  33. package/dist/packer/linux/linux.d.ts +2 -0
  34. package/dist/packer/mac/mac.d.ts +23 -0
  35. package/dist/packer/win/nsis.d.ts +24 -0
  36. package/dist/packer/win/resedit.d.ts +5 -0
  37. package/dist/packer/win/toolchain.d.ts +8 -0
  38. package/dist/packer/win/win.d.ts +12 -0
  39. package/dist/pipeline/pipeline.d.ts +41 -0
  40. package/dist/pipeline/targets.d.ts +28 -0
  41. package/dist/shared/hash.d.ts +2 -0
  42. package/dist/shared/logger.d.ts +39 -0
  43. package/dist/shared/platform.d.ts +26 -0
  44. package/dist/shared/types.d.ts +68 -0
  45. package/dist/update/blockmap.d.ts +13 -0
  46. package/dist/update/metadata.d.ts +20 -0
  47. package/dist/update/version.d.ts +7 -0
  48. package/package.json +71 -0
  49. package/src/archive/ar.ts +41 -0
  50. package/src/archive/seven-za.ts +160 -0
  51. package/src/archive/tar.ts +142 -0
  52. package/src/archive/zip-reader.ts +61 -0
  53. package/src/asar/dependency/collect.ts +437 -0
  54. package/src/asar/dependency/detector.ts +73 -0
  55. package/src/asar/dependency/hoister.ts +714 -0
  56. package/src/asar/dependency/utils.ts +225 -0
  57. package/src/asar/fileset/matcher.ts +101 -0
  58. package/src/asar/fileset/platform-fileset.ts +416 -0
  59. package/src/asar/fileset/transformer.ts +154 -0
  60. package/src/asar/fileset/unpack-detector.ts +38 -0
  61. package/src/asar/fileset/walker.ts +115 -0
  62. package/src/asar/pack.ts +186 -0
  63. package/src/asar/packer/app-collector.ts +74 -0
  64. package/src/asar/packer/collector.ts +134 -0
  65. package/src/asar/packer/concurrency.ts +24 -0
  66. package/src/asar/packer/integrity.ts +53 -0
  67. package/src/asar/packer/nm-collector.ts +194 -0
  68. package/src/asar/packer/stream-builder.ts +723 -0
  69. package/src/cli.ts +182 -0
  70. package/src/config/index.ts +365 -0
  71. package/src/electron/dist-zip.ts +65 -0
  72. package/src/electron/download.ts +121 -0
  73. package/src/index.ts +14 -0
  74. package/src/packer/common/app-update-yml.ts +21 -0
  75. package/src/packer/common/extra-resources.ts +83 -0
  76. package/src/packer/common/staging.ts +33 -0
  77. package/src/packer/common/template.ts +24 -0
  78. package/src/packer/linux/linux.ts +329 -0
  79. package/src/packer/mac/mac.ts +495 -0
  80. package/src/packer/win/nsis.ts +244 -0
  81. package/src/packer/win/resedit.ts +60 -0
  82. package/src/packer/win/toolchain.ts +63 -0
  83. package/src/packer/win/win.ts +421 -0
  84. package/src/pipeline/pipeline.ts +149 -0
  85. package/src/pipeline/targets.ts +67 -0
  86. package/src/shared/hash.ts +15 -0
  87. package/src/shared/logger.ts +91 -0
  88. package/src/shared/platform.ts +41 -0
  89. package/src/shared/types.ts +82 -0
  90. package/src/update/blockmap.ts +247 -0
  91. package/src/update/metadata.ts +97 -0
  92. package/src/update/version.ts +59 -0
  93. package/templates/linux/app.desktop +11 -0
  94. package/templates/linux/control +9 -0
  95. package/templates/linux/postinst +10 -0
  96. package/templates/linux/postrm +8 -0
  97. package/templates/linux/preinst +49 -0
  98. package/templates/linux/prerm +43 -0
  99. package/templates/mac/Info.plist +18 -0
  100. package/templates/mac/app-update.yml +4 -0
  101. package/templates/win/nsis/README.md +51 -0
  102. package/templates/win/nsis/THIRD-PARTY-NOTICES.md +20 -0
  103. package/templates/win/nsis/assistedInstaller.nsh +154 -0
  104. package/templates/win/nsis/assistedMessages.yml +368 -0
  105. package/templates/win/nsis/common.nsh +147 -0
  106. package/templates/win/nsis/empty-license.txt +1 -0
  107. package/templates/win/nsis/include/FileAssociation.nsh +123 -0
  108. package/templates/win/nsis/include/StdUtils.nsh +496 -0
  109. package/templates/win/nsis/include/StrContains.nsh +48 -0
  110. package/templates/win/nsis/include/UAC.nsh +299 -0
  111. package/templates/win/nsis/include/allowOnlyOneInstallerInstance.nsh +164 -0
  112. package/templates/win/nsis/include/extractAppPackage.nsh +138 -0
  113. package/templates/win/nsis/include/getProcessInfo.nsh +157 -0
  114. package/templates/win/nsis/include/installUtil.nsh +248 -0
  115. package/templates/win/nsis/include/installer.nsh +247 -0
  116. package/templates/win/nsis/include/nsProcess.nsh +28 -0
  117. package/templates/win/nsis/include/webPackage.nsh +64 -0
  118. package/templates/win/nsis/installSection.nsh +110 -0
  119. package/templates/win/nsis/installer.nsi +132 -0
  120. package/templates/win/nsis/messages.yml +240 -0
  121. package/templates/win/nsis/multiUser.nsh +134 -0
  122. package/templates/win/nsis/multiUserUi.nsh +230 -0
  123. package/templates/win/nsis/oneClick.nsh +19 -0
  124. package/templates/win/nsis/portable.nsi +91 -0
  125. package/templates/win/nsis/uninstaller.nsh +263 -0
  126. package/templates/win/nsis-header.nsh +57 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * macOS .app/zip packer.
3
+ */
4
+ import { createWriteStream, existsSync, lstatSync, readdirSync, readlinkSync } from 'node:fs'
5
+ import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
6
+ import { createRequire } from 'node:module'
7
+ import os from 'node:os'
8
+ import path from 'node:path'
9
+ import type { Archiver, ArchiverOptions } from 'archiver'
10
+ import { build as buildPlist, type PlistValue, parse as parsePlist } from 'plist'
11
+ import {
12
+ getEntryBuffer,
13
+ getSymlinkTarget,
14
+ isDefaultAppEntry,
15
+ isSymlink,
16
+ type ZipEntry,
17
+ } from '../../archive/zip-reader.ts'
18
+ import { hashHeader } from '../../asar/packer/integrity.ts'
19
+ import type { NormalizedConfig } from '../../config/index.ts'
20
+ import { downloadAndReadElectron } from '../../electron/dist-zip.ts'
21
+ import type { PackerContext } from '../../pipeline/targets.ts'
22
+ import { logger } from '../../shared/logger.ts'
23
+ import { NodePlatform, nodeArchName } from '../../shared/platform.ts'
24
+ import { buildBlockMap } from '../../update/blockmap.ts'
25
+ import { generateUpdateMetadata } from '../../update/metadata.ts'
26
+ import { renderAppUpdateYml } from '../common/app-update-yml.ts'
27
+ import {
28
+ type ExtraResourceEntry,
29
+ isExtraResourcePathExcluded,
30
+ resolveExtraResources,
31
+ } from '../common/extra-resources.ts'
32
+ import { createStagingDir, removeStagingDir } from '../common/staging.ts'
33
+ import { getTemplatesDir } from '../common/template.ts'
34
+
35
+ const require = createRequire(import.meta.url)
36
+ const { ZipArchive } = require('archiver') as {
37
+ ZipArchive: new (options: ArchiverOptions) => Archiver
38
+ }
39
+
40
+ const TEMPLATES_DIR = getTemplatesDir('mac')
41
+
42
+ interface MacBundleIdentity {
43
+ /** Bundle directory name, for example "MyApp.app". */
44
+ appName: string
45
+ /** On-disk bundle base name. */
46
+ bundleName: string
47
+ /** User-visible product name. */
48
+ displayName: string
49
+ executableName: string
50
+ helperName: string
51
+ /** Helper name with unsafe characters removed. */
52
+ sanitizedHelperName: string
53
+ }
54
+
55
+ interface HelperPlistEntry {
56
+ executable: string
57
+ displayName: string
58
+ name: string
59
+ bundleId: string
60
+ version: string
61
+ }
62
+
63
+ export async function packMac({
64
+ config,
65
+ arch,
66
+ outputDir,
67
+ asarPath,
68
+ unpackedDir,
69
+ }: PackerContext): Promise<string> {
70
+ if (asarPath === undefined) throw new Error('packMac: asarPath is required')
71
+ logger.info(`\n[macOS] ${arch} packaging started`)
72
+
73
+ const stagingDir = await createStagingDir(outputDir, 'mac', arch)
74
+
75
+ try {
76
+ logger.info(' [1/6] Downloading and reading macOS Electron dist ...')
77
+ const { entries } = await downloadAndReadElectron(NodePlatform.DARWIN, arch, config)
78
+
79
+ logger.info(' [2/6] Generating Info.plist and computing asar integrity ...')
80
+ const identity = resolveMacBundleIdentity(config)
81
+ const asarHash = await computeAsarHash(asarPath)
82
+ const defaultAppAsarHash = await computeDefaultAppAsarHash(entries)
83
+ const mainPlist = await generateMainPlist(config, identity, { asarHash, defaultAppAsarHash })
84
+ const helperPlistMap = buildHelperPlistMap(config, identity)
85
+
86
+ logger.info(' [3/6] Writing zip (in-memory assembly) ...')
87
+ const zipFilename = `${config.artifactName}_${config.version}_${nodeArchName(arch)}_unsigned.zip`
88
+ const zipPath = path.join(outputDir, zipFilename)
89
+ await writeMacZip({
90
+ entries,
91
+ identity,
92
+ mainPlist,
93
+ helperPlistMap,
94
+ asarPath,
95
+ unpackedDir,
96
+ zipPath,
97
+ config,
98
+ })
99
+ logger.info(` [macOS] ${arch} zip complete: ${zipPath}`)
100
+
101
+ let distributablePath = zipPath
102
+
103
+ if (config.sign.enabled && config.sign.mac?.enabled && config.sign.mac?.hook) {
104
+ logger.info(' [4/6] Running sign hook ...')
105
+ distributablePath =
106
+ (await config.sign.mac.hook(zipPath, { config, arch, outputDir })) || zipPath
107
+ }
108
+
109
+ if (config.update.generateMetadata) {
110
+ logger.info(' [6/6] Generating update metadata ...')
111
+ const zipBlockmapPath = `${distributablePath}.blockmap`
112
+ let blockMapSize: number | undefined
113
+ try {
114
+ blockMapSize = (await buildBlockMap(distributablePath, 'gzip', zipBlockmapPath))
115
+ .blockMapSize
116
+ } catch (e) {
117
+ console.warn(
118
+ ` [macOS] Warning: blockmap generation failed: ${e instanceof Error ? e.message : String(e)}`,
119
+ )
120
+ }
121
+ generateUpdateMetadata({
122
+ filePath: distributablePath,
123
+ version: config.version,
124
+ outputDir,
125
+ platform: NodePlatform.DARWIN,
126
+ blockMapSize,
127
+ channel: config.update.channel,
128
+ extraFiles: config.update.extraArtifacts,
129
+ })
130
+ }
131
+
132
+ logger.info(` [macOS] ${arch} packaging complete: ${distributablePath}`)
133
+ return distributablePath
134
+ } finally {
135
+ await removeStagingDir(stagingDir)
136
+ }
137
+ }
138
+
139
+ /** Derive bundle identity (app name, executable, helper names) from config. */
140
+ export function resolveMacBundleIdentity(config: NormalizedConfig): MacBundleIdentity {
141
+ const displayName = config.productName.trim()
142
+ const bundleName = config.mac.bundleName.trim() || displayName
143
+ const executableName = bundleName
144
+ const helperName = displayName
145
+
146
+ return {
147
+ appName: `${bundleName}.app`,
148
+ bundleName,
149
+ displayName,
150
+ executableName,
151
+ helperName,
152
+ sanitizedHelperName: helperName.replace(/[^\w()\- ]/g, ''),
153
+ }
154
+ }
155
+
156
+ /** Normalize an archive entry mode to executable / regular file bits. */
157
+ export function normalizeMacArchiveFileMode(sourceMode: string | number): number {
158
+ return Number(sourceMode) & 0o111 ? 0o100755 : 0o100644
159
+ }
160
+
161
+ export function addDirectoryToArchive(
162
+ archive: Archiver,
163
+ dirPath: string,
164
+ archivePath: string,
165
+ resource: ExtraResourceEntry = {},
166
+ relativePath = '',
167
+ ): void {
168
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
169
+ const fullPath = path.join(dirPath, entry.name)
170
+ const entryArchivePath = `${archivePath}/${entry.name}`
171
+ const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name
172
+
173
+ if (isExtraResourcePathExcluded(resource, entryRelativePath)) continue
174
+
175
+ if (entry.isDirectory()) {
176
+ archive.append('', { name: `${entryArchivePath}/`, mode: 0o40755 })
177
+ addDirectoryToArchive(archive, fullPath, entryArchivePath, resource, entryRelativePath)
178
+ } else if (entry.isSymbolicLink()) {
179
+ archive.symlink(entryArchivePath, readlinkSync(fullPath), 0o120755)
180
+ } else if (entry.isFile()) {
181
+ archive.file(fullPath, {
182
+ name: entryArchivePath,
183
+ mode: normalizeMacArchiveFileMode(lstatSync(fullPath).mode),
184
+ })
185
+ }
186
+ }
187
+ }
188
+
189
+ /** Build Electron helper-process rename rules for the sanitized helper name. */
190
+ function buildRenameRules(sanitizedHelperName: string): { from: string; to: string }[] {
191
+ const rules: { from: string; to: string }[] = []
192
+ const helperSuffixes = [
193
+ ' Helper (Renderer)',
194
+ ' Helper (Plugin)',
195
+ ' Helper (GPU)',
196
+ ' Helper EH',
197
+ ' Helper NP',
198
+ ' Helper',
199
+ ]
200
+ for (const suffix of helperSuffixes) {
201
+ rules.push({ from: `Electron${suffix}`, to: `${sanitizedHelperName}${suffix}` })
202
+ }
203
+ rules.push({ from: 'Electron Login Helper', to: `${sanitizedHelperName} Login Helper` })
204
+ return rules
205
+ }
206
+
207
+ function applyRenameRules(pathStr: string, rules: { from: string; to: string }[]): string {
208
+ const sorted = [...rules].sort((a, b) => b.from.length - a.from.length)
209
+ let result = pathStr
210
+ for (const { from, to } of sorted) {
211
+ if (result.includes(from)) {
212
+ result = result.split(from).join(to)
213
+ }
214
+ }
215
+ return result
216
+ }
217
+
218
+ async function writeMacZip({
219
+ entries,
220
+ identity,
221
+ mainPlist,
222
+ helperPlistMap,
223
+ asarPath,
224
+ unpackedDir,
225
+ zipPath,
226
+ config,
227
+ }: {
228
+ entries: ZipEntry[]
229
+ identity: MacBundleIdentity
230
+ mainPlist: string
231
+ helperPlistMap: Record<string, HelperPlistEntry>
232
+ asarPath: string
233
+ unpackedDir?: string
234
+ zipPath: string
235
+ config: NormalizedConfig
236
+ }): Promise<void> {
237
+ const oldAppPrefix = 'Electron.app/'
238
+ const finalAppName = identity.appName
239
+ const productFilename = identity.executableName
240
+ const sanitizedHelperName = identity.sanitizedHelperName
241
+ const renameRules = buildRenameRules(sanitizedHelperName)
242
+
243
+ const iconSrc = config.mac.iconFile ? path.join(config.projectDir, config.mac.iconFile) : null
244
+ const iconBuffer = iconSrc && existsSync(iconSrc) ? await readFile(iconSrc) : null
245
+
246
+ const output = createWriteStream(zipPath)
247
+ const archive = new ZipArchive({ zlib: { level: 9 } })
248
+
249
+ const writePromise = new Promise<void>((resolve, reject) => {
250
+ output.on('close', () => resolve())
251
+ output.on('error', reject)
252
+ archive.on('error', reject)
253
+ })
254
+ archive.pipe(output)
255
+
256
+ const writtenDirs = new Set<string>()
257
+
258
+ function ensureDir(archiveDirPath: string, mode: string | number): void {
259
+ if (!writtenDirs.has(archiveDirPath)) {
260
+ writtenDirs.add(archiveDirPath)
261
+ archive.append('', { name: `${archiveDirPath}/`, mode: Number(mode) || 0o40755 })
262
+ }
263
+ }
264
+
265
+ for (const e of entries) {
266
+ const oldPath = e.path
267
+ if (!oldPath.startsWith(oldAppPrefix)) continue
268
+
269
+ if (isDefaultAppEntry(oldPath)) continue
270
+ if (oldPath === `${oldAppPrefix}Contents/Resources/electron.icns`) continue
271
+ if (/^Electron\.app\/Contents\/Resources\/[^/]+\.lproj\/?$/.test(oldPath)) continue
272
+
273
+ let newPath = oldPath.replace(oldAppPrefix, `${finalAppName}/`)
274
+
275
+ const mainExeOldPath = `${finalAppName}/Contents/MacOS/Electron`
276
+ if (newPath === mainExeOldPath) {
277
+ newPath = `${finalAppName}/Contents/MacOS/${productFilename}`
278
+ } else if (newPath.includes('/Electron Helper') || newPath.includes('/Electron Login Helper')) {
279
+ newPath = applyRenameRules(newPath, renameRules)
280
+ }
281
+
282
+ const pathParts = newPath.split('/')
283
+ for (let i = 1; i < pathParts.length; i++) {
284
+ ensureDir(pathParts.slice(0, i).join('/'), e.mode)
285
+ }
286
+
287
+ if (isSymlink(e)) {
288
+ const target = await getSymlinkTarget(e.entry)
289
+ let newTarget = target
290
+ const isHelper =
291
+ newPath.includes(`/${sanitizedHelperName} Helper`) ||
292
+ newPath.includes(`/${sanitizedHelperName} Login Helper`)
293
+ if (isHelper) {
294
+ newTarget = applyRenameRules(newTarget, renameRules)
295
+ }
296
+ archive.symlink(newPath, newTarget, 0o120755)
297
+ continue
298
+ }
299
+
300
+ if (e.dir) {
301
+ ensureDir(newPath.replace(/\/$/, ''), e.mode)
302
+ continue
303
+ }
304
+
305
+ if (newPath === `${finalAppName}/Contents/Info.plist`) {
306
+ archive.append(mainPlist, { name: newPath, mode: 0o100644 })
307
+ continue
308
+ }
309
+
310
+ const helperPlistEntry = helperPlistMap[newPath]
311
+ if (helperPlistEntry) {
312
+ const helperInfo = parsePlist(await e.entry.async('text')) as Record<string, PlistValue>
313
+ helperInfo.CFBundleExecutable = helperPlistEntry.executable
314
+ helperInfo.CFBundleDisplayName = helperPlistEntry.displayName
315
+ helperInfo.CFBundleName = helperPlistEntry.name
316
+ helperInfo.CFBundleIdentifier = helperPlistEntry.bundleId
317
+ helperInfo.CFBundleVersion = helperPlistEntry.version
318
+ archive.append(buildPlist(helperInfo), { name: newPath, mode: 0o100644 })
319
+ continue
320
+ }
321
+
322
+ const buffer = await getEntryBuffer(e.entry)
323
+ archive.append(buffer, { name: newPath, mode: Number(e.mode) })
324
+ }
325
+
326
+ if (iconBuffer) {
327
+ archive.append(iconBuffer, {
328
+ name: `${finalAppName}/Contents/Resources/icon.icns`,
329
+ mode: 0o100644,
330
+ })
331
+ }
332
+
333
+ if (existsSync(asarPath)) {
334
+ const asarStat = await stat(asarPath)
335
+ if (asarStat.size > 2 * 1024 * 1024 * 1024) {
336
+ archive.file(asarPath, {
337
+ name: `${finalAppName}/Contents/Resources/app.asar`,
338
+ mode: 0o100644,
339
+ })
340
+ } else {
341
+ archive.append(await readFile(asarPath), {
342
+ name: `${finalAppName}/Contents/Resources/app.asar`,
343
+ mode: 0o100644,
344
+ })
345
+ }
346
+ }
347
+
348
+ if (unpackedDir && existsSync(unpackedDir)) {
349
+ archive.append('', {
350
+ name: `${finalAppName}/Contents/Resources/app.asar.unpacked/`,
351
+ mode: 0o40755,
352
+ })
353
+ addDirectoryToArchive(
354
+ archive,
355
+ unpackedDir,
356
+ `${finalAppName}/Contents/Resources/app.asar.unpacked`,
357
+ )
358
+ }
359
+
360
+ await injectExtraResourcesToZip(archive, finalAppName, config)
361
+
362
+ if (config.update.generateMetadata && config.update.url) {
363
+ const updateYml = await renderAppUpdateYml(config)
364
+ archive.append(updateYml, {
365
+ name: `${finalAppName}/Contents/Resources/app-update.yml`,
366
+ mode: 0o100644,
367
+ })
368
+ }
369
+
370
+ await archive.finalize()
371
+ await writePromise
372
+ }
373
+
374
+ function buildHelperPlistMap(
375
+ config: NormalizedConfig,
376
+ identity: MacBundleIdentity,
377
+ ): Record<string, HelperPlistEntry> {
378
+ const map: Record<string, HelperPlistEntry> = {}
379
+ const helperBundleId = `${config.appId}.helper`
380
+ const finalAppName = identity.appName
381
+ const helpers = [
382
+ { suffix: ' Helper', bundleSuffix: '' },
383
+ { suffix: ' Helper (Renderer)', bundleSuffix: '.(Renderer)' },
384
+ { suffix: ' Helper (Plugin)', bundleSuffix: '.(Plugin)' },
385
+ { suffix: ' Helper (GPU)', bundleSuffix: '.(GPU)' },
386
+ { suffix: ' Helper EH', bundleSuffix: '.EH' },
387
+ { suffix: ' Helper NP', bundleSuffix: '.NP' },
388
+ ]
389
+
390
+ for (const h of helpers) {
391
+ const plistPath = `${finalAppName}/Contents/Frameworks/${identity.sanitizedHelperName}${h.suffix}.app/Contents/Info.plist`
392
+ map[plistPath] = {
393
+ executable: `${identity.sanitizedHelperName}${h.suffix}`,
394
+ displayName: `${identity.displayName}${h.suffix}`,
395
+ name: `${identity.displayName}${h.suffix}`,
396
+ bundleId: `${helperBundleId}${h.bundleSuffix}`,
397
+ version: config.version,
398
+ }
399
+ }
400
+
401
+ const loginPlistPath = `${finalAppName}/Contents/Library/LoginItems/${identity.sanitizedHelperName} Login Helper.app/Contents/Info.plist`
402
+ map[loginPlistPath] = {
403
+ executable: `${identity.sanitizedHelperName} Login Helper`,
404
+ displayName: `${identity.displayName} Login Helper`,
405
+ name: `${identity.displayName} Login Helper`,
406
+ bundleId: `${config.appId}.loginhelper`,
407
+ version: config.version,
408
+ }
409
+
410
+ return map
411
+ }
412
+
413
+ async function generateMainPlist(
414
+ config: NormalizedConfig,
415
+ identity: MacBundleIdentity,
416
+ { asarHash, defaultAppAsarHash }: { asarHash: string; defaultAppAsarHash: string },
417
+ ): Promise<string> {
418
+ const buildVersion = config.version.includes('.') ? config.version : `${config.version}.0`
419
+ const year = new Date().getFullYear()
420
+
421
+ const templateXml = await readFile(path.join(TEMPLATES_DIR, 'Info.plist'), 'utf8')
422
+ const info = parsePlist(templateXml) as Record<string, PlistValue>
423
+
424
+ info.CFBundleDisplayName = identity.displayName
425
+ info.CFBundleExecutable = identity.executableName
426
+ info.CFBundleIconFile = config.mac.iconFile ? 'icon.icns' : 'electron.icns'
427
+ info.CFBundleIdentifier = config.appId
428
+ info.CFBundleName = identity.bundleName
429
+ info.CFBundleShortVersionString = config.version
430
+ info.CFBundleVersion = buildVersion
431
+ info.ElectronAsarIntegrity = {
432
+ 'Resources/app.asar': { algorithm: 'SHA256', hash: asarHash },
433
+ 'Resources/default_app.asar': { algorithm: 'SHA256', hash: defaultAppAsarHash },
434
+ }
435
+ info.LSApplicationCategoryType = config.mac.category
436
+ info.LSMinimumSystemVersion = config.mac.minimumSystemVersion
437
+ info.NSHumanReadableCopyright = `Copyright © ${year} ${config.author}`.trim()
438
+
439
+ if (config.mac.urlSchemes?.length) {
440
+ info.CFBundleURLTypes = config.mac.urlSchemes.map((scheme) => ({
441
+ CFBundleTypeRole: 'Editor',
442
+ CFBundleURLName: config.productName,
443
+ CFBundleURLSchemes: [scheme],
444
+ }))
445
+ }
446
+
447
+ if (config.mac.plist && Object.keys(config.mac.plist).length > 0) {
448
+ Object.assign(info, config.mac.plist)
449
+ }
450
+
451
+ return buildPlist(info)
452
+ }
453
+
454
+ async function computeAsarHash(asarPath: string): Promise<string> {
455
+ const { hash } = await hashHeader(asarPath)
456
+ return hash
457
+ }
458
+
459
+ async function computeDefaultAppAsarHash(entries: ZipEntry[]): Promise<string> {
460
+ // Find the default_app.asar entry inside the Electron distribution zip.
461
+ const entry = entries.find((e) => e.path === 'Electron.app/Contents/Resources/default_app.asar')
462
+ if (!entry) {
463
+ throw new Error('default_app.asar not found in Electron zip')
464
+ }
465
+ const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'cross-packer-asar-'))
466
+ const tmpAsar = path.join(tmpDir, 'default_app.asar')
467
+ try {
468
+ await writeFile(tmpAsar, await getEntryBuffer(entry.entry))
469
+ const { hash } = await hashHeader(tmpAsar)
470
+ return hash
471
+ } finally {
472
+ await rm(tmpDir, { recursive: true, force: true })
473
+ }
474
+ }
475
+
476
+ async function injectExtraResourcesToZip(
477
+ archive: Archiver,
478
+ finalAppName: string,
479
+ config: NormalizedConfig,
480
+ ): Promise<void> {
481
+ const resourcesPrefix = `${finalAppName}/Contents/Resources`
482
+
483
+ for (const { res, srcPath, srcStat, destPath: resTo } of await resolveExtraResources(config)) {
484
+ const archivePath = `${resourcesPrefix}/${resTo}`
485
+ if (srcStat.isDirectory()) {
486
+ archive.append('', { name: `${archivePath}/`, mode: 0o40755 })
487
+ addDirectoryToArchive(archive, srcPath, archivePath, res)
488
+ } else {
489
+ archive.append(await readFile(srcPath), {
490
+ name: archivePath,
491
+ mode: normalizeMacArchiveFileMode(srcStat.mode),
492
+ })
493
+ }
494
+ }
495
+ }