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,121 @@
1
+ /**
2
+ * Electron distribution download + cache + checksum validation.
3
+ */
4
+ import { createHash } from 'node:crypto'
5
+ import {
6
+ createReadStream,
7
+ createWriteStream,
8
+ existsSync,
9
+ mkdirSync,
10
+ renameSync,
11
+ rmSync,
12
+ } from 'node:fs'
13
+ import http from 'node:http'
14
+ import https from 'node:https'
15
+ import path from 'node:path'
16
+ import { Arch, type NodePlatform } from '../shared/platform.ts'
17
+
18
+ export interface DownloadElectronOptions {
19
+ version: string
20
+ platform: NodePlatform
21
+ arch: Arch
22
+ mirror?: string
23
+ cacheDir?: string
24
+ expectedChecksum: string
25
+ }
26
+
27
+ /**
28
+ * Compose the Electron distribution archive name for a target. The Electron
29
+ * release artifacts use "x64" rather than "amd64" for the x86-64 architecture,
30
+ * for example version "33.2.0" yields "electron-v33.2.0-win32-x64.zip".
31
+ */
32
+ export function electronArchiveName(version: string, platform: NodePlatform, arch: Arch): string {
33
+ const electronArch = arch === Arch.AMD64 ? 'x64' : arch
34
+ return `electron-v${version}-${platform}-${electronArch}.zip`
35
+ }
36
+
37
+ /** Streams a file through sha256 and compares against the expected hex digest. */
38
+ export async function validateArchiveChecksum(
39
+ archivePath: string,
40
+ expectedChecksum: string,
41
+ ): Promise<void> {
42
+ const hash = createHash('sha256')
43
+ for await (const chunk of createReadStream(archivePath)) {
44
+ hash.update(chunk)
45
+ }
46
+ const actual = hash.digest('hex')
47
+ if (actual !== expectedChecksum) {
48
+ throw new Error(
49
+ `Checksum mismatch for ${archivePath}: expected ${expectedChecksum}, got ${actual}`,
50
+ )
51
+ }
52
+ }
53
+
54
+ /** Download a file with cache support and return the cached archive path. */
55
+ export async function downloadElectron({
56
+ version,
57
+ platform,
58
+ arch,
59
+ mirror = 'https://github.com/electron/electron/releases/download/',
60
+ cacheDir,
61
+ expectedChecksum,
62
+ }: DownloadElectronOptions): Promise<string> {
63
+ const zipName = electronArchiveName(version, platform, arch)
64
+ const base = mirror.endsWith('/') ? mirror : `${mirror}/`
65
+ const url = `${base}v${version}/${zipName}`
66
+
67
+ const dir = cacheDir ?? path.join(process.cwd(), '.electron-cache')
68
+ const cachePath = path.join(dir, zipName)
69
+
70
+ if (existsSync(cachePath)) {
71
+ try {
72
+ await validateArchiveChecksum(cachePath, expectedChecksum)
73
+ return cachePath
74
+ } catch {
75
+ rmSync(cachePath, { force: true })
76
+ }
77
+ }
78
+
79
+ mkdirSync(dir, { recursive: true })
80
+ const tmpPath = `${cachePath}.downloading`
81
+ try {
82
+ await downloadFile(url, tmpPath)
83
+ await validateArchiveChecksum(tmpPath, expectedChecksum)
84
+ renameSync(tmpPath, cachePath)
85
+ } catch (err) {
86
+ rmSync(tmpPath, { force: true })
87
+ throw err
88
+ }
89
+ return cachePath
90
+ }
91
+
92
+ /** Download a file while following redirects. No external dependencies. */
93
+ export function downloadFile(url: string, destPath: string): Promise<void> {
94
+ return new Promise((resolve, reject) => {
95
+ const follow = (current: string): void => {
96
+ const client = current.startsWith('https') ? https : http
97
+ client
98
+ .get(current, { timeout: 300_000 }, (res) => {
99
+ const statusCode = res.statusCode ?? 0
100
+ if (statusCode >= 300 && statusCode < 400 && res.headers.location) {
101
+ follow(res.headers.location)
102
+ return
103
+ }
104
+ if (statusCode !== 200) {
105
+ reject(new Error(`Download failed: HTTP ${statusCode} from ${current}`))
106
+ return
107
+ }
108
+ const file = createWriteStream(destPath)
109
+ res.pipe(file)
110
+ file.on('finish', () => {
111
+ file.close()
112
+ resolve()
113
+ })
114
+ file.on('error', reject)
115
+ res.on('error', reject)
116
+ })
117
+ .on('error', reject)
118
+ }
119
+ follow(url)
120
+ })
121
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export type { PrebuiltModuleRule } from './asar/fileset/platform-fileset.ts'
2
+ export { setPrebuiltDirResolver } from './asar/fileset/platform-fileset.ts'
3
+ export type {
4
+ LoadConfigOverrides,
5
+ NormalizedConfig,
6
+ } from './config/index.ts'
7
+ export { loadConfig, normalizeConfig, resolveConfigPath } from './config/index.ts'
8
+ export type { ExtraResourceEntry } from './packer/common/extra-resources.ts'
9
+ export type { PackResult, PipelineOptions, TargetResult, TargetSpec } from './pipeline/pipeline.ts'
10
+ export { runPack } from './pipeline/pipeline.ts'
11
+ export { Arch, NodePlatform, nodeArchName, Platform } from './shared/platform.ts'
12
+ export { buildBlockMap } from './update/blockmap.ts'
13
+ export type { UpdateMetadataOptions } from './update/metadata.ts'
14
+ export { generateUpdateMetadata } from './update/metadata.ts'
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared app-update.yml rendering, used by the win and mac packers.
3
+ */
4
+ import { readFile } from 'node:fs/promises'
5
+ import type { NormalizedConfig } from '../../config/index.ts'
6
+ import { getTemplatesDir, renderTemplate } from './template.ts'
7
+
8
+ /** Derive the electron-updater cache directory name for a config. */
9
+ export function getUpdaterCacheDirName(config: NormalizedConfig): string {
10
+ return `${config.name.toLowerCase()}-updater`
11
+ }
12
+
13
+ /** Render the shared app-update.yml template. */
14
+ export async function renderAppUpdateYml(config: NormalizedConfig): Promise<string> {
15
+ const template = await readFile(getTemplatesDir('mac', 'app-update.yml'), 'utf8')
16
+ return renderTemplate(template, {
17
+ updateServerUrl: config.update.url,
18
+ updaterCacheDirName: getUpdaterCacheDirName(config),
19
+ channel: config.update.channel,
20
+ })
21
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Extra-resource entry filtering, shared by the win/mac/linux packers.
3
+ */
4
+ import { existsSync, type Stats } from 'node:fs'
5
+ import { stat } from 'node:fs/promises'
6
+ import path from 'node:path'
7
+ import type { CollectDirectoryOptions } from '../../archive/tar.ts'
8
+ import type { NormalizedConfig } from '../../config/index.ts'
9
+
10
+ export interface ExtraResourceEntry {
11
+ from?: string
12
+ to?: string
13
+ required?: boolean
14
+ exclude?: string[]
15
+ collectOptions?: CollectDirectoryOptions
16
+ }
17
+
18
+ /** A resolved extra resource: source path, stat, and in-archive destination. */
19
+ export interface ResolvedExtraResource {
20
+ res: ExtraResourceEntry
21
+ srcPath: string
22
+ srcStat: Stats
23
+ destPath: string
24
+ }
25
+
26
+ /**
27
+ * Resolve every configured extra resource into a source path plus stat.
28
+ * Missing optional sources are skipped; required sources throw via
29
+ * assertRequiredExtraResource.
30
+ */
31
+ export async function resolveExtraResources(
32
+ config: NormalizedConfig,
33
+ ): Promise<ResolvedExtraResource[]> {
34
+ const resolved: ResolvedExtraResource[] = []
35
+ for (const res of config.extraResources) {
36
+ if (!res.from || !res.to) continue
37
+ assertRequiredExtraResource(config.projectDir, res)
38
+ const srcPath = path.join(config.projectDir, res.from)
39
+ if (!existsSync(srcPath)) continue
40
+ resolved.push({ res, srcPath, srcStat: await stat(srcPath), destPath: res.to })
41
+ }
42
+ return resolved
43
+ }
44
+
45
+ /**
46
+ * Check whether a directory entry (relative to the resource root) matches
47
+ * the resource's exclude list. A matched path or any path under it is excluded.
48
+ */
49
+ export function isExtraResourcePathExcluded(
50
+ resource: ExtraResourceEntry,
51
+ relativePath: string,
52
+ ): boolean {
53
+ const normalized = relativePath.replaceAll('\\', '/').replace(/^\.\//, '')
54
+ return (resource.exclude || []).some(
55
+ (excludedPath) =>
56
+ normalized === excludedPath || normalized.startsWith(`${excludedPath.replace(/\/$/, '')}/`),
57
+ )
58
+ }
59
+
60
+ /**
61
+ * Ensures required extra resources exist before packaging.
62
+ * The optional hint callback returns an appended instruction telling the
63
+ * user how to obtain the missing resource (e.g. a project-specific
64
+ * download command); returning undefined falls back to the plain message.
65
+ * @throws when a required resource path is missing
66
+ */
67
+ export function assertRequiredExtraResource(
68
+ projectDir: string,
69
+ res: ExtraResourceEntry,
70
+ missingHint?: (from: string) => string | undefined,
71
+ ): void {
72
+ if (!res.required) return
73
+
74
+ const srcPath = path.join(projectDir, res.from ?? '')
75
+ if (!existsSync(srcPath)) {
76
+ const hint = missingHint?.(res.from ?? '')
77
+ throw new Error(
78
+ hint
79
+ ? `Required extra resource not found: ${res.from}. ${hint}`
80
+ : `Required extra resource not found: ${res.from}`,
81
+ )
82
+ }
83
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Per-target staging directory creation and removal, shared by the platform
3
+ * packers.
4
+ */
5
+ import { mkdir, rm } from 'node:fs/promises'
6
+ import path from 'node:path'
7
+ import { logger } from '../../shared/logger.ts'
8
+ import type { Arch } from '../../shared/platform.ts'
9
+
10
+ export async function createStagingDir(
11
+ outputDir: string,
12
+ label: string,
13
+ arch: Arch,
14
+ ): Promise<string> {
15
+ const stagingDir = path.join(outputDir, `cross-packer-${label}-${arch}-staging`)
16
+ await mkdir(stagingDir, { recursive: true })
17
+ return stagingDir
18
+ }
19
+
20
+ export async function removeStagingDir(stagingDir: string): Promise<void> {
21
+ try {
22
+ await rm(stagingDir, {
23
+ recursive: true,
24
+ force: true,
25
+ maxRetries: 5,
26
+ retryDelay: 1000,
27
+ })
28
+ } catch (err) {
29
+ logger.warn(
30
+ `Could not remove staging directory ${stagingDir}: ${err instanceof Error ? err.message : String(err)}`,
31
+ )
32
+ }
33
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Generic {{placeholder}} template rendering with CRLF normalization.
3
+ */
4
+ import path from 'node:path'
5
+
6
+ export function getTemplatesDir(...segments: string[]): string {
7
+ return path.join(import.meta.dirname, '..', '..', '..', 'templates', ...segments)
8
+ }
9
+
10
+ export function renderTemplate(
11
+ template: string,
12
+ replacements: Record<string, string | number>,
13
+ ): string {
14
+ let rendered = template
15
+ for (const [key, value] of Object.entries(replacements)) {
16
+ rendered = rendered.replaceAll(`{{${key}}}`, String(value))
17
+ }
18
+ return normalizeUnixLineEndings(rendered)
19
+ }
20
+
21
+ /** Normalize CRLF and CR line endings to LF. */
22
+ function normalizeUnixLineEndings(content: string): string {
23
+ return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
24
+ }
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Linux .deb packer.
3
+ */
4
+ import { existsSync, readFileSync, type Stats } from 'node:fs'
5
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
6
+ import path from 'node:path'
7
+ import { type ReadEntry, create as tarCreate } from 'tar'
8
+ import { type ArEntry, createArArchive } from '../../archive/ar.ts'
9
+ import { compress } from '../../archive/seven-za.ts'
10
+ import { collectDirectoryEntries, type TarEntry, writeTarFromEntries } from '../../archive/tar.ts'
11
+ import {
12
+ getEntryBuffer,
13
+ getSymlinkTarget,
14
+ isDefaultAppEntry,
15
+ isSymlink,
16
+ type ZipEntry,
17
+ } from '../../archive/zip-reader.ts'
18
+ import type { NormalizedConfig } from '../../config/index.ts'
19
+ import { downloadAndReadElectron } from '../../electron/dist-zip.ts'
20
+ import type { PackerContext } from '../../pipeline/targets.ts'
21
+ import { logger } from '../../shared/logger.ts'
22
+ import { type Arch, NodePlatform } from '../../shared/platform.ts'
23
+ import { generateUpdateMetadata } from '../../update/metadata.ts'
24
+ import { isExtraResourcePathExcluded, resolveExtraResources } from '../common/extra-resources.ts'
25
+ import { createStagingDir, removeStagingDir } from '../common/staging.ts'
26
+ import { getTemplatesDir, renderTemplate } from '../common/template.ts'
27
+
28
+ const TEMPLATES_DIR = getTemplatesDir('linux')
29
+
30
+ export async function packLinux({
31
+ config,
32
+ arch,
33
+ outputDir,
34
+ asarPath,
35
+ unpackedDir,
36
+ }: PackerContext): Promise<string> {
37
+ if (asarPath === undefined) throw new Error('packLinux: asarPath is required')
38
+ logger.info(`\n[Linux] ${arch} packaging started`)
39
+ const stagingDir = await createStagingDir(outputDir, 'linux', arch)
40
+
41
+ try {
42
+ logger.info(' [1/5] Downloading and reading Linux Electron dist ...')
43
+ const { entries } = await downloadAndReadElectron(NodePlatform.LINUX, arch, config)
44
+
45
+ logger.info(' [2/5] Building data.tar.xz ...')
46
+ const installPrefix = `opt/${config.linux.installDir}`
47
+ const dataEntries = buildDataEntries(entries, { config, installPrefix, asarPath })
48
+ await appendUnpackedEntries(dataEntries, unpackedDir, installPrefix)
49
+ dataEntries.push(await createDesktopEntry(config))
50
+ dataEntries.push(...(await collectIconEntries(config)))
51
+ dataEntries.push(...(await collectExtraResourceEntries(config)))
52
+
53
+ const dataTarPath = path.join(stagingDir, 'data.tar.xz')
54
+ await writeDataTarXz(dataEntries, dataTarPath, stagingDir)
55
+
56
+ const installedSize = Math.ceil((await stat(dataTarPath)).size / 1024)
57
+
58
+ logger.info(' [3/5] Building control.tar.xz ...')
59
+ const controlDir = path.join(stagingDir, 'control')
60
+ await mkdir(controlDir, { recursive: true })
61
+ await createControlFiles(controlDir, config, arch, installedSize)
62
+ const controlTarPath = path.join(stagingDir, 'control.tar.xz')
63
+ await writeControlTarXz(controlDir, controlTarPath, stagingDir)
64
+
65
+ logger.info(' [4/5] Assembling .deb ...')
66
+ const debFilename = `${config.artifactName}_${config.version}_${arch}.deb`
67
+ const debPath = path.join(outputDir, debFilename)
68
+ const arFiles: ArEntry[] = [
69
+ { name: 'debian-binary', content: Buffer.from('2.0\n'), mode: 0o100644 },
70
+ { name: 'control.tar.xz', content: await readFile(controlTarPath), mode: 0o100644 },
71
+ { name: 'data.tar.xz', content: await readFile(dataTarPath), mode: 0o100644 },
72
+ ]
73
+ await writeFile(debPath, createArArchive(arFiles))
74
+
75
+ if (config.update.generateMetadata) {
76
+ logger.info(' [5/5] Generating update metadata ...')
77
+ generateUpdateMetadata({
78
+ filePath: debPath,
79
+ version: config.version,
80
+ outputDir,
81
+ platform: NodePlatform.LINUX,
82
+ channel: config.update.channel,
83
+ })
84
+ }
85
+
86
+ logger.info(` [Linux] ${arch} packaging complete: ${debPath}`)
87
+ return debPath
88
+ } finally {
89
+ await removeStagingDir(stagingDir)
90
+ }
91
+ }
92
+
93
+ function buildDataEntries(
94
+ electronEntries: ZipEntry[],
95
+ {
96
+ config,
97
+ installPrefix,
98
+ asarPath,
99
+ }: { config: NormalizedConfig; installPrefix: string; asarPath: string },
100
+ ): TarEntry[] {
101
+ const dataEntries: TarEntry[] = []
102
+
103
+ for (const e of electronEntries) {
104
+ // The default application is replaced by the user's asar.
105
+ if (isDefaultAppEntry(e.path)) continue
106
+
107
+ let destPath = e.path
108
+ if (e.path === 'electron') destPath = config.name
109
+
110
+ const fullDestPath = `${installPrefix}/${destPath}`
111
+
112
+ if (e.dir) {
113
+ dataEntries.push({ type: 'dir', path: fullDestPath, mode: Number(e.mode) || 0o40755 })
114
+ } else if (isSymlink(e)) {
115
+ dataEntries.push({
116
+ type: 'link',
117
+ path: fullDestPath,
118
+ mode: Number(e.mode) || 0o120755,
119
+ getTarget: () => getSymlinkTarget(e.entry),
120
+ })
121
+ } else {
122
+ dataEntries.push({
123
+ type: 'file',
124
+ path: fullDestPath,
125
+ mode: Number(e.mode) || 0o100644,
126
+ getBuffer: () => getEntryBuffer(e.entry),
127
+ })
128
+ }
129
+ }
130
+
131
+ dataEntries.push({
132
+ type: 'file',
133
+ path: `${installPrefix}/resources/app.asar`,
134
+ mode: 0o100644,
135
+ filePath: asarPath,
136
+ })
137
+
138
+ return dataEntries
139
+ }
140
+
141
+ async function appendUnpackedEntries(
142
+ dataEntries: TarEntry[],
143
+ unpackedDir: string | undefined,
144
+ installPrefix: string,
145
+ ): Promise<void> {
146
+ if (!unpackedDir || !existsSync(unpackedDir)) return
147
+ dataEntries.push(
148
+ ...(await collectDirectoryEntries(unpackedDir, `${installPrefix}/resources/app.asar.unpacked`)),
149
+ )
150
+ }
151
+
152
+ /** Render the .desktop entry from the template and config. */
153
+ async function createDesktopEntry(config: NormalizedConfig): Promise<TarEntry> {
154
+ const template = readFileSync(path.join(TEMPLATES_DIR, 'app.desktop'), 'utf8')
155
+ const mimeType = config.linux.mimeType ? `MimeType=${config.linux.mimeType};\n` : ''
156
+
157
+ let desktopExtra = ''
158
+ if (config.linux.desktopExtraFile) {
159
+ const fragment = readFileSync(
160
+ path.join(config.projectDir, config.linux.desktopExtraFile),
161
+ 'utf8',
162
+ )
163
+ desktopExtra = `${renderTemplate(fragment, {
164
+ productName: config.productName,
165
+ description: config.description || config.productName,
166
+ name: config.name,
167
+ installDir: config.linux.installDir,
168
+ }).trimEnd()}\n`
169
+ }
170
+ const content = renderTemplate(template, {
171
+ productName: config.productName,
172
+ description: config.description || config.productName,
173
+ name: config.name,
174
+ installDir: config.linux.installDir,
175
+ mimeType,
176
+ desktopExtra,
177
+ })
178
+ return {
179
+ type: 'file',
180
+ path: `usr/share/applications/${config.name}.desktop`,
181
+ mode: 0o100644,
182
+ content: Buffer.from(content),
183
+ }
184
+ }
185
+
186
+ /** Collect hicolor icon entries for every configured icon size. */
187
+ async function collectIconEntries(config: NormalizedConfig): Promise<TarEntry[]> {
188
+ const entries: TarEntry[] = []
189
+ const iconDir = config.linux.iconDir
190
+
191
+ for (const size of config.linux.iconSizes) {
192
+ const iconSrc = path.join(config.projectDir, iconDir, `${size}x${size}.png`)
193
+ if (!existsSync(iconSrc)) {
194
+ throw new Error(`Missing Linux icon (${size}x${size}): ${iconSrc}`)
195
+ }
196
+ entries.push({
197
+ type: 'file',
198
+ path: `usr/share/icons/hicolor/${size}x${size}/apps/${config.name}.png`,
199
+ mode: 0o100644,
200
+ filePath: iconSrc,
201
+ })
202
+ }
203
+
204
+ if (entries.length > 0) logger.info(' Collected Linux icons')
205
+ return entries
206
+ }
207
+
208
+ /** Collect extra resource entries for the /opt install prefix. */
209
+ async function collectExtraResourceEntries(config: NormalizedConfig): Promise<TarEntry[]> {
210
+ const entries: TarEntry[] = []
211
+ const installPrefix = `opt/${config.linux.installDir}`
212
+
213
+ for (const { res, srcPath, srcStat, destPath: resTo } of await resolveExtraResources(config)) {
214
+ const destPrefix = `${installPrefix}/resources/${resTo}`
215
+ if (srcStat.isDirectory()) {
216
+ const dirEntries = await collectDirectoryEntries(srcPath, destPrefix, res.collectOptions)
217
+ entries.push(
218
+ ...dirEntries.filter(
219
+ (entry) => !isExtraResourcePathExcluded(res, entry.path.slice(destPrefix.length + 1)),
220
+ ),
221
+ )
222
+ } else {
223
+ entries.push({ type: 'file', path: destPrefix, mode: 0o100644, filePath: srcPath })
224
+ }
225
+ }
226
+
227
+ if (entries.length > 0) logger.info(' Collected Linux extra resources')
228
+ return entries
229
+ }
230
+
231
+ async function createControlFiles(
232
+ controlDir: string,
233
+ config: NormalizedConfig,
234
+ debArch: Arch,
235
+ installedSize: number,
236
+ ): Promise<void> {
237
+ const depends = (config.linux.debDepends || []).join(', ')
238
+ const recommends = config.linux.debRecommends ? `Recommends: ${config.linux.debRecommends}\n` : ''
239
+
240
+ const controlTemplate = await readFile(path.join(TEMPLATES_DIR, 'control'), 'utf8')
241
+ const controlContent = renderTemplate(controlTemplate, {
242
+ name: config.name,
243
+ version: config.version,
244
+ debArch,
245
+ maintainer: config.linux.maintainer || config.author || 'Unknown',
246
+ depends,
247
+ recommends,
248
+ installedSize,
249
+ description: config.description || config.productName,
250
+ })
251
+ await writeFile(path.join(controlDir, 'control'), controlContent)
252
+
253
+ const scriptReplacements = {
254
+ productName: config.productName,
255
+ name: config.name,
256
+ installDir: config.linux.installDir,
257
+ }
258
+ await writeMaintainerScript(
259
+ controlDir,
260
+ 'preinst',
261
+ path.join(TEMPLATES_DIR, 'preinst'),
262
+ scriptReplacements,
263
+ )
264
+ await writeMaintainerScript(
265
+ controlDir,
266
+ 'postinst',
267
+ path.join(TEMPLATES_DIR, 'postinst'),
268
+ scriptReplacements,
269
+ )
270
+ await writeMaintainerScript(
271
+ controlDir,
272
+ 'prerm',
273
+ path.join(TEMPLATES_DIR, 'prerm'),
274
+ scriptReplacements,
275
+ )
276
+ await writeMaintainerScript(controlDir, 'postrm', path.join(TEMPLATES_DIR, 'postrm'), {
277
+ installDir: config.linux.installDir,
278
+ })
279
+ }
280
+
281
+ async function writeDataTarXz(
282
+ entries: TarEntry[],
283
+ destPath: string,
284
+ stagingDir: string,
285
+ ): Promise<void> {
286
+ const tarPath = path.join(stagingDir, 'data.tar')
287
+ await writeTarFromEntries(entries, tarPath)
288
+ compress({ src: tarPath, dest: destPath, format: 'xz', level: 9 })
289
+ }
290
+
291
+ async function writeControlTarXz(
292
+ controlDir: string,
293
+ destPath: string,
294
+ stagingDir: string,
295
+ ): Promise<void> {
296
+ const tarPath = path.join(stagingDir, 'control.tar')
297
+ await tarCreate(
298
+ {
299
+ gzip: false,
300
+ file: tarPath,
301
+ cwd: controlDir,
302
+ portable: true,
303
+ noPax: true,
304
+ filter: (entryPath: string, entryStat: ReadEntry | Stats) => {
305
+ const p = entryPath.replace(/^\.\//, '')
306
+ if (p === '.') return true
307
+ entryStat.mode =
308
+ p === 'preinst' || p === 'postinst' || p === 'prerm' || p === 'postrm'
309
+ ? 0o100755
310
+ : 0o100644
311
+ return true
312
+ },
313
+ },
314
+ ['.'],
315
+ )
316
+ compress({ src: tarPath, dest: destPath, format: 'xz', level: 9 })
317
+ }
318
+
319
+ /** Render a maintainer script template and write it into the deb control dir. */
320
+ async function writeMaintainerScript(
321
+ controlDir: string,
322
+ fileName: string,
323
+ templatePath: string,
324
+ replacements: Record<string, string | number>,
325
+ ): Promise<void> {
326
+ const template = await readFile(templatePath, 'utf8')
327
+ const content = renderTemplate(template, replacements)
328
+ await writeFile(path.join(controlDir, fileName), content, { encoding: 'utf8' })
329
+ }