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,41 @@
1
+ import { Buffer } from 'node:buffer'
2
+
3
+ export interface ArEntry {
4
+ name: string
5
+ content: Buffer
6
+ mode?: number
7
+ mtime?: number
8
+ }
9
+
10
+ /**
11
+ * Minimal `ar` archive writer, sufficient for .deb assembly
12
+ * (debian-binary + control.tar.xz + data.tar.xz).
13
+ */
14
+ export function createArArchive(files: ArEntry[]): Buffer {
15
+ const parts = [Buffer.from('!<arch>\n')]
16
+
17
+ for (const file of files) {
18
+ const mtime = file.mtime ?? Math.floor(Date.now() / 1000)
19
+ const mode = file.mode ?? 0o100644
20
+ const size = file.content.length
21
+
22
+ const header = Buffer.alloc(60)
23
+ header.write(file.name.padEnd(16), 0, 16, 'ascii')
24
+ header.write(String(mtime).padEnd(12), 16, 12, 'ascii')
25
+ header.write('0 ', 28, 6, 'ascii')
26
+ header.write('0 ', 34, 6, 'ascii')
27
+ header.write(mode.toString(8).padStart(6, '0').padEnd(8), 40, 8, 'ascii')
28
+ header.write(String(size).padEnd(10), 48, 10, 'ascii')
29
+ header.write('`\n', 58, 2, 'ascii')
30
+
31
+ parts.push(header)
32
+ parts.push(Buffer.from(file.content))
33
+
34
+ // ar members are 2-byte aligned
35
+ if (size % 2 !== 0) {
36
+ parts.push(Buffer.from('\n'))
37
+ }
38
+ }
39
+
40
+ return Buffer.concat(parts)
41
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * 7za / xz compression wrapper.
3
+ * 7za is provided by the `7zip-bin` npm package, which is available on
4
+ * every host operating system. On Linux, xz compression uses the system
5
+ * `xz` executable when available.
6
+ */
7
+ import { type SpawnSyncReturns, spawnSync } from 'node:child_process'
8
+ import { existsSync, renameSync } from 'node:fs'
9
+ import os from 'node:os'
10
+ import { path7za } from '7zip-bin'
11
+
12
+ const DEFAULT_7Z_LEVEL = 5
13
+
14
+ export interface CompressOptions {
15
+ src: string
16
+ dest: string
17
+ format?: '7z' | 'xz'
18
+ level?: number
19
+ threads?: number
20
+ solid?: boolean
21
+ sevenZipPath?: string
22
+ }
23
+
24
+ export interface CompressResult {
25
+ level: number
26
+ threads: number
27
+ solid: boolean
28
+ }
29
+
30
+ export function get7zaPath(): string {
31
+ if (existsSync(path7za)) return path7za
32
+ throw new Error(`7za not found: ${path7za}`)
33
+ }
34
+
35
+ /**
36
+ * Parses `7z l` output for the total uncompressed size, used for the
37
+ * NSIS ESTIMATED_SIZE define. Returns 0 when listing fails.
38
+ */
39
+ export function computeEstimatedSize(sevenZipPath: string, archiveFile: string): number {
40
+ const result = spawnSync(sevenZipPath, ['l', archiveFile], {
41
+ encoding: 'utf8',
42
+ stdio: 'pipe',
43
+ windowsHide: true,
44
+ })
45
+ if (result.status !== 0) return 0
46
+ const output = (result.stdout || '').trim()
47
+ const match = /(\d+)\s+\d+\s+\d+\s+files/.exec(output)
48
+ return match ? Number.parseInt(match[1], 10) : 0
49
+ }
50
+
51
+ export function compress({
52
+ src,
53
+ dest,
54
+ format = '7z',
55
+ level = DEFAULT_7Z_LEVEL,
56
+ threads,
57
+ solid = false,
58
+ sevenZipPath: sevenZipPathOption,
59
+ }: CompressOptions): CompressResult {
60
+ const resolved = resolveCompressionOptions({ level, threads, solid })
61
+
62
+ if (format === 'xz' && process.platform === 'linux' && hasSystemXz()) {
63
+ compressWithSystemXz({ src, dest, level: resolved.level })
64
+ return resolved
65
+ }
66
+
67
+ const sevenZipPath = sevenZipPathOption ?? get7zaPath()
68
+ const { args, cwd } = build7zaAddArgs({ src, dest, format, ...resolved })
69
+
70
+ const result = spawnSync(sevenZipPath, args, { cwd, stdio: 'pipe', windowsHide: true })
71
+ if (result.status !== 0) {
72
+ throwCompressionError(format, result)
73
+ }
74
+ return resolved
75
+ }
76
+
77
+ function resolveCompressionOptions(options: {
78
+ level?: number
79
+ threads?: number
80
+ solid?: boolean
81
+ }): CompressResult {
82
+ const level = clampLevel(options.level ?? DEFAULT_7Z_LEVEL)
83
+ const threads = Math.max(1, options.threads ?? os.cpus().length)
84
+ const solid = options.solid ?? false
85
+
86
+ return { level, threads, solid }
87
+ }
88
+
89
+ export function build7zaAddArgs({
90
+ src,
91
+ dest,
92
+ format = '7z',
93
+ level,
94
+ threads,
95
+ solid,
96
+ }: {
97
+ src: string
98
+ dest: string
99
+ format?: '7z' | 'xz'
100
+ level: number
101
+ threads: number
102
+ solid: boolean
103
+ }): { args: string[]; cwd: string | undefined } {
104
+ const formatArg = format === '7z' ? [] : [`-t${format}`]
105
+ const srcArg = format === '7z' ? '.' : src
106
+ const args = ['a', '-bd', `-mx=${level}`, ...formatArg, dest, srcArg]
107
+
108
+ if (format === '7z') {
109
+ args.splice(3, 0, `-mmt=${threads}`)
110
+ if (!solid) {
111
+ args.splice(3, 0, '-ms=off')
112
+ }
113
+ }
114
+
115
+ return { args, cwd: format === '7z' ? src : undefined }
116
+ }
117
+
118
+ /** Clamp a compression level to the valid 0-9 range of 7z. */
119
+ function clampLevel(level: number): number {
120
+ if (!Number.isFinite(level)) {
121
+ return DEFAULT_7Z_LEVEL
122
+ }
123
+ return Math.min(9, Math.max(0, Math.trunc(level)))
124
+ }
125
+
126
+ function hasSystemXz(): boolean {
127
+ const result = spawnSync('xz', ['--version'], { stdio: 'ignore', windowsHide: true })
128
+ return result.status === 0
129
+ }
130
+
131
+ function compressWithSystemXz({
132
+ src,
133
+ dest,
134
+ level,
135
+ }: {
136
+ src: string
137
+ dest: string
138
+ level: number
139
+ }): void {
140
+ const result = spawnSync('xz', [`-${level}`, '-k', '-f', src], {
141
+ stdio: 'pipe',
142
+ windowsHide: true,
143
+ })
144
+ if (result.status !== 0) {
145
+ throwCompressionError('xz', result)
146
+ }
147
+ const defaultDest = `${src}.xz`
148
+ if (dest !== defaultDest) {
149
+ renameSync(defaultDest, dest)
150
+ }
151
+ }
152
+
153
+ function throwCompressionError(format: string, result: SpawnSyncReturns<Buffer>): never {
154
+ if (result.error) {
155
+ throw new Error(`${format} compression failed to spawn: ${result.error.message}`)
156
+ }
157
+ const stderr = result.stderr?.toString().trim() || ''
158
+ const signal = result.signal ? `, signal ${result.signal}` : ''
159
+ throw new Error(`${format} compression failed (exit ${result.status}${signal}): ${stderr}`)
160
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Tar entry helpers: materialize entries into a temporary directory, then
3
+ * pack it with `tar` while preserving per-entry modes. Mode preservation is
4
+ * required for setuid binaries such as chrome-sandbox.
5
+ */
6
+ import { existsSync, type Stats } from 'node:fs'
7
+ import { mkdir, readdir, readFile, readlink, rm, stat, symlink, writeFile } from 'node:fs/promises'
8
+ import path from 'node:path'
9
+ import type { ReadEntry } from 'tar'
10
+ import * as tar from 'tar'
11
+
12
+ export type TarEntry =
13
+ | { type: 'dir'; path: string; mode?: number }
14
+ | {
15
+ type: 'file'
16
+ path: string
17
+ mode?: number
18
+ content?: Buffer
19
+ filePath?: string
20
+ getBuffer?: () => Promise<Buffer>
21
+ }
22
+ | {
23
+ type: 'link'
24
+ path: string
25
+ mode?: number
26
+ target?: string
27
+ getTarget?: () => Promise<string>
28
+ }
29
+
30
+ /** Packs entries into an uncompressed tar at destPath. */
31
+ export async function writeTarFromEntries(entries: TarEntry[], destPath: string): Promise<void> {
32
+ const tmpDir = `${destPath}-tmp`
33
+ await mkdir(tmpDir, { recursive: true })
34
+
35
+ try {
36
+ const modeMap = new Map<string, number | undefined>()
37
+
38
+ for (const entry of entries) {
39
+ const fullPath = path.join(tmpDir, entry.path)
40
+ const parentDir = path.dirname(fullPath)
41
+
42
+ if (entry.type === 'dir') {
43
+ await mkdir(fullPath, { recursive: true })
44
+ modeMap.set(entry.path, entry.mode)
45
+ } else if (entry.type === 'file') {
46
+ await mkdir(parentDir, { recursive: true })
47
+ await writeFile(fullPath, await resolveBuffer(entry))
48
+ modeMap.set(entry.path, entry.mode)
49
+ } else if (entry.type === 'link') {
50
+ await mkdir(parentDir, { recursive: true })
51
+ const target = entry.getTarget ? await entry.getTarget() : entry.target || ''
52
+ await symlink(target, fullPath)
53
+ modeMap.set(entry.path, entry.mode)
54
+ }
55
+ }
56
+
57
+ await tar.create(
58
+ {
59
+ gzip: false,
60
+ file: destPath,
61
+ cwd: tmpDir,
62
+ portable: true,
63
+ noPax: true,
64
+ filter: (entryPath: string, stat: ReadEntry | Stats) => {
65
+ const p = entryPath.replace(/^\.\//, '')
66
+ if (p === '.') return true
67
+ const mode = modeMap.get(p)
68
+ if (mode !== undefined) {
69
+ stat.mode = mode
70
+ }
71
+ return true
72
+ },
73
+ },
74
+ ['.'],
75
+ )
76
+ } finally {
77
+ await rm(tmpDir, { recursive: true, force: true })
78
+ }
79
+ }
80
+
81
+ export interface CollectDirectoryOptions {
82
+ includeSymlinks?: boolean
83
+ resolveFileMode?: (input: {
84
+ filePath: string
85
+ relativePath: string
86
+ sourceMode: number
87
+ }) => number | undefined | undefined
88
+ }
89
+
90
+ /** Flattens a directory on disk into tar entries rooted at prefix. */
91
+ export async function collectDirectoryEntries(
92
+ dirPath: string,
93
+ prefix: string,
94
+ options: CollectDirectoryOptions = {},
95
+ ): Promise<TarEntry[]> {
96
+ if (!existsSync(dirPath)) return []
97
+
98
+ const entries: TarEntry[] = []
99
+ await walkDir(dirPath, dirPath, prefix, entries, options)
100
+ return entries
101
+ }
102
+
103
+ /** Resolve a file entry's content: inline Buffer, disk path, or lazy getter. */
104
+ async function resolveBuffer(entry: Extract<TarEntry, { type: 'file' }>): Promise<Buffer> {
105
+ if (entry.content) return entry.content
106
+ if (entry.filePath) return readFile(entry.filePath)
107
+ if (entry.getBuffer) return entry.getBuffer()
108
+ throw new Error(`TarEntry "${entry.path}" has no content source`)
109
+ }
110
+
111
+ async function walkDir(
112
+ rootDir: string,
113
+ dirPath: string,
114
+ prefix: string,
115
+ entries: TarEntry[],
116
+ options: CollectDirectoryOptions,
117
+ ): Promise<void> {
118
+ const children = await readdir(dirPath, { withFileTypes: true })
119
+ for (const child of children) {
120
+ const fullPath = path.join(dirPath, child.name)
121
+ const entryPath = `${prefix}/${child.name}`
122
+ const relativePath = path.relative(rootDir, fullPath).split(path.sep).join('/')
123
+
124
+ if (child.isDirectory()) {
125
+ entries.push({ type: 'dir', path: entryPath, mode: 0o40755 })
126
+ await walkDir(rootDir, fullPath, entryPath, entries, options)
127
+ } else if (child.isFile()) {
128
+ const s = await stat(fullPath)
129
+ const mode =
130
+ options.resolveFileMode?.({ filePath: fullPath, relativePath, sourceMode: s.mode }) ??
131
+ 0o100644
132
+ entries.push({ type: 'file', path: entryPath, mode, filePath: fullPath })
133
+ } else if (child.isSymbolicLink() && options.includeSymlinks) {
134
+ entries.push({
135
+ type: 'link',
136
+ path: entryPath,
137
+ mode: 0o120755,
138
+ target: await readlink(fullPath),
139
+ })
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Reads a zip archive (Electron distributions) preserving Unix permissions
3
+ * so symlinks and setuid bits survive repacking.
4
+ */
5
+ import { readFile } from 'node:fs/promises'
6
+ import type { JSZipObject } from 'jszip'
7
+ import JSZip from 'jszip'
8
+
9
+ const S_IFMT = 0o170000
10
+ const S_IFLNK = 0o120000
11
+
12
+ export interface ZipEntry {
13
+ path: string
14
+ dir: boolean
15
+ symlink: boolean
16
+ mode: string | number
17
+ entry: JSZipObject
18
+ }
19
+
20
+ export async function readZip(zipPath: string): Promise<{ entries: ZipEntry[]; zip: JSZip }> {
21
+ const data = await readFile(zipPath)
22
+ const zip = await JSZip.loadAsync(data)
23
+
24
+ const entries: ZipEntry[] = []
25
+ for (const [entryPath, entry] of Object.entries(zip.files)) {
26
+ const unixPerms = Number(entry.unixPermissions || 0)
27
+ const fileType = unixPerms & S_IFMT
28
+
29
+ entries.push({
30
+ path: entryPath,
31
+ dir: entry.dir,
32
+ symlink: fileType === S_IFLNK,
33
+ mode: unixPerms,
34
+ entry,
35
+ })
36
+ }
37
+
38
+ return { entries, zip }
39
+ }
40
+
41
+ export async function getEntryBuffer(entry: JSZipObject): Promise<Buffer> {
42
+ return entry.async('nodebuffer')
43
+ }
44
+
45
+ export async function getSymlinkTarget(entry: JSZipObject): Promise<string> {
46
+ return entry.async('text')
47
+ }
48
+
49
+ export function isSymlink(zipEntry: ZipEntry): boolean {
50
+ return (Number(zipEntry.mode) & S_IFMT) === S_IFLNK
51
+ }
52
+
53
+ export function isDefaultAppEntry(entryPath: string): boolean {
54
+ return (
55
+ entryPath === 'resources/app' ||
56
+ entryPath.startsWith('resources/app/') ||
57
+ entryPath === 'resources/default_app.asar' ||
58
+ entryPath === 'Electron.app/Contents/Resources/app' ||
59
+ entryPath.startsWith('Electron.app/Contents/Resources/app/')
60
+ )
61
+ }