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.
- package/LICENSE +661 -0
- package/README.md +151 -0
- package/README.zh-CN.md +151 -0
- package/dist/archive/ar.d.ts +12 -0
- package/dist/archive/seven-za.d.ts +32 -0
- package/dist/archive/tar.d.ts +30 -0
- package/dist/archive/zip-reader.d.ts +17 -0
- package/dist/asar/dependency/collect.d.ts +2 -0
- package/dist/asar/dependency/detector.d.ts +8 -0
- package/dist/asar/dependency/hoister.d.ts +27 -0
- package/dist/asar/dependency/utils.d.ts +59 -0
- package/dist/asar/fileset/matcher.d.ts +12 -0
- package/dist/asar/fileset/platform-fileset.d.ts +36 -0
- package/dist/asar/fileset/transformer.d.ts +2 -0
- package/dist/asar/fileset/unpack-detector.d.ts +10 -0
- package/dist/asar/fileset/walker.d.ts +14 -0
- package/dist/asar/pack.d.ts +23 -0
- package/dist/asar/packer/app-collector.d.ts +2 -0
- package/dist/asar/packer/collector.d.ts +18 -0
- package/dist/asar/packer/concurrency.d.ts +1 -0
- package/dist/asar/packer/integrity.d.ts +8 -0
- package/dist/asar/packer/nm-collector.d.ts +7 -0
- package/dist/asar/packer/stream-builder.d.ts +18 -0
- package/dist/cli.d.ts +2 -0
- package/dist/config/index.d.ts +97 -0
- package/dist/electron/dist-zip.d.ts +18 -0
- package/dist/electron/download.d.ts +21 -0
- package/dist/index.d.ts +11 -0
- package/dist/packer/common/app-update-yml.d.ts +5 -0
- package/dist/packer/common/extra-resources.d.ts +39 -0
- package/dist/packer/common/staging.d.ts +3 -0
- package/dist/packer/common/template.d.ts +2 -0
- package/dist/packer/linux/linux.d.ts +2 -0
- package/dist/packer/mac/mac.d.ts +23 -0
- package/dist/packer/win/nsis.d.ts +24 -0
- package/dist/packer/win/resedit.d.ts +5 -0
- package/dist/packer/win/toolchain.d.ts +8 -0
- package/dist/packer/win/win.d.ts +12 -0
- package/dist/pipeline/pipeline.d.ts +41 -0
- package/dist/pipeline/targets.d.ts +28 -0
- package/dist/shared/hash.d.ts +2 -0
- package/dist/shared/logger.d.ts +39 -0
- package/dist/shared/platform.d.ts +26 -0
- package/dist/shared/types.d.ts +68 -0
- package/dist/update/blockmap.d.ts +13 -0
- package/dist/update/metadata.d.ts +20 -0
- package/dist/update/version.d.ts +7 -0
- package/package.json +71 -0
- package/src/archive/ar.ts +41 -0
- package/src/archive/seven-za.ts +160 -0
- package/src/archive/tar.ts +142 -0
- package/src/archive/zip-reader.ts +61 -0
- package/src/asar/dependency/collect.ts +437 -0
- package/src/asar/dependency/detector.ts +73 -0
- package/src/asar/dependency/hoister.ts +714 -0
- package/src/asar/dependency/utils.ts +225 -0
- package/src/asar/fileset/matcher.ts +101 -0
- package/src/asar/fileset/platform-fileset.ts +416 -0
- package/src/asar/fileset/transformer.ts +154 -0
- package/src/asar/fileset/unpack-detector.ts +38 -0
- package/src/asar/fileset/walker.ts +115 -0
- package/src/asar/pack.ts +186 -0
- package/src/asar/packer/app-collector.ts +74 -0
- package/src/asar/packer/collector.ts +134 -0
- package/src/asar/packer/concurrency.ts +24 -0
- package/src/asar/packer/integrity.ts +53 -0
- package/src/asar/packer/nm-collector.ts +194 -0
- package/src/asar/packer/stream-builder.ts +723 -0
- package/src/cli.ts +182 -0
- package/src/config/index.ts +365 -0
- package/src/electron/dist-zip.ts +65 -0
- package/src/electron/download.ts +121 -0
- package/src/index.ts +14 -0
- package/src/packer/common/app-update-yml.ts +21 -0
- package/src/packer/common/extra-resources.ts +83 -0
- package/src/packer/common/staging.ts +33 -0
- package/src/packer/common/template.ts +24 -0
- package/src/packer/linux/linux.ts +329 -0
- package/src/packer/mac/mac.ts +495 -0
- package/src/packer/win/nsis.ts +244 -0
- package/src/packer/win/resedit.ts +60 -0
- package/src/packer/win/toolchain.ts +63 -0
- package/src/packer/win/win.ts +421 -0
- package/src/pipeline/pipeline.ts +149 -0
- package/src/pipeline/targets.ts +67 -0
- package/src/shared/hash.ts +15 -0
- package/src/shared/logger.ts +91 -0
- package/src/shared/platform.ts +41 -0
- package/src/shared/types.ts +82 -0
- package/src/update/blockmap.ts +247 -0
- package/src/update/metadata.ts +97 -0
- package/src/update/version.ts +59 -0
- package/templates/linux/app.desktop +11 -0
- package/templates/linux/control +9 -0
- package/templates/linux/postinst +10 -0
- package/templates/linux/postrm +8 -0
- package/templates/linux/preinst +49 -0
- package/templates/linux/prerm +43 -0
- package/templates/mac/Info.plist +18 -0
- package/templates/mac/app-update.yml +4 -0
- package/templates/win/nsis/README.md +51 -0
- package/templates/win/nsis/THIRD-PARTY-NOTICES.md +20 -0
- package/templates/win/nsis/assistedInstaller.nsh +154 -0
- package/templates/win/nsis/assistedMessages.yml +368 -0
- package/templates/win/nsis/common.nsh +147 -0
- package/templates/win/nsis/empty-license.txt +1 -0
- package/templates/win/nsis/include/FileAssociation.nsh +123 -0
- package/templates/win/nsis/include/StdUtils.nsh +496 -0
- package/templates/win/nsis/include/StrContains.nsh +48 -0
- package/templates/win/nsis/include/UAC.nsh +299 -0
- package/templates/win/nsis/include/allowOnlyOneInstallerInstance.nsh +164 -0
- package/templates/win/nsis/include/extractAppPackage.nsh +138 -0
- package/templates/win/nsis/include/getProcessInfo.nsh +157 -0
- package/templates/win/nsis/include/installUtil.nsh +248 -0
- package/templates/win/nsis/include/installer.nsh +247 -0
- package/templates/win/nsis/include/nsProcess.nsh +28 -0
- package/templates/win/nsis/include/webPackage.nsh +64 -0
- package/templates/win/nsis/installSection.nsh +110 -0
- package/templates/win/nsis/installer.nsi +132 -0
- package/templates/win/nsis/messages.yml +240 -0
- package/templates/win/nsis/multiUser.nsh +134 -0
- package/templates/win/nsis/multiUserUi.nsh +230 -0
- package/templates/win/nsis/oneClick.nsh +19 -0
- package/templates/win/nsis/portable.nsi +91 -0
- package/templates/win/nsis/uninstaller.nsh +263 -0
- package/templates/win/nsis-header.nsh +57 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows NSIS packer.
|
|
3
|
+
* The NSIS build runs in two phases: phase 1 emits the uninstaller, and
|
|
4
|
+
* phase 2 emits the installer.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import crypto from 'node:crypto'
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import { cp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { compress, computeEstimatedSize, get7zaPath } from '../../archive/seven-za.ts'
|
|
12
|
+
import { getEntryBuffer, isDefaultAppEntry, type ZipEntry } from '../../archive/zip-reader.ts'
|
|
13
|
+
import type { NormalizedConfig } from '../../config/index.ts'
|
|
14
|
+
import { downloadAndReadElectron } from '../../electron/dist-zip.ts'
|
|
15
|
+
import type { PackerContext } from '../../pipeline/targets.ts'
|
|
16
|
+
import { hashFile } from '../../shared/hash.ts'
|
|
17
|
+
import { logger } from '../../shared/logger.ts'
|
|
18
|
+
import { Arch, NodePlatform } from '../../shared/platform.ts'
|
|
19
|
+
import { buildBlockMap } from '../../update/blockmap.ts'
|
|
20
|
+
import { generateUpdateMetadata } from '../../update/metadata.ts'
|
|
21
|
+
import { getBuildVersion } from '../../update/version.ts'
|
|
22
|
+
import { getUpdaterCacheDirName, renderAppUpdateYml } from '../common/app-update-yml.ts'
|
|
23
|
+
import { isExtraResourcePathExcluded, resolveExtraResources } from '../common/extra-resources.ts'
|
|
24
|
+
import { createStagingDir, removeStagingDir } from '../common/staging.ts'
|
|
25
|
+
import {
|
|
26
|
+
buildNsisScriptHeader,
|
|
27
|
+
executeMakensis,
|
|
28
|
+
getNsisTemplatesDir,
|
|
29
|
+
prepareNsisToolchain,
|
|
30
|
+
} from './nsis.ts'
|
|
31
|
+
import { editExeResources } from './resedit.ts'
|
|
32
|
+
|
|
33
|
+
export async function packWin({
|
|
34
|
+
config,
|
|
35
|
+
arch,
|
|
36
|
+
outputDir,
|
|
37
|
+
asarPath,
|
|
38
|
+
unpackedDir,
|
|
39
|
+
}: PackerContext): Promise<string> {
|
|
40
|
+
if (asarPath === undefined) throw new Error('packWin: asarPath is required')
|
|
41
|
+
logger.info(`\n[Windows] ${arch} packaging started`)
|
|
42
|
+
|
|
43
|
+
const stagingDir = await createStagingDir(outputDir, 'win', arch)
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
logger.info(' [1/11] Downloading and reading Windows Electron dist ...')
|
|
47
|
+
const { entries } = await downloadAndReadElectron(NodePlatform.WIN32, arch, config)
|
|
48
|
+
|
|
49
|
+
logger.info(' [2/11] Assembling app directory ...')
|
|
50
|
+
const appDir = path.join(stagingDir, 'app')
|
|
51
|
+
await mkdir(appDir, { recursive: true })
|
|
52
|
+
await assembleAppDir({ entries, appDir, asarPath, unpackedDir, config, arch })
|
|
53
|
+
|
|
54
|
+
logger.info(' [3/11] Setting exe icon and version info ...')
|
|
55
|
+
const productExePath = path.join(appDir, `${config.productName}.exe`)
|
|
56
|
+
const iconSrc = config.win.iconFile ? path.join(config.projectDir, config.win.iconFile) : null
|
|
57
|
+
const iconPath = iconSrc && existsSync(iconSrc) ? iconSrc : null
|
|
58
|
+
await editExeResources(productExePath, { iconPath, config })
|
|
59
|
+
|
|
60
|
+
if (config.sign.enabled && config.sign.win?.enabled && config.sign.win?.hook) {
|
|
61
|
+
logger.info(' [4/11] Running sign hook on app directory ...')
|
|
62
|
+
await config.sign.win.hook(appDir, { config, arch, outputDir })
|
|
63
|
+
} else {
|
|
64
|
+
logger.info(' [4/11] Signing disabled, skipping app directory binaries')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logger.info(' [5/11] Creating 7z archive ...')
|
|
68
|
+
const sevenZipPath = get7zaPath()
|
|
69
|
+
const archiveFile = path.join(stagingDir, `app-${arch}.7z`)
|
|
70
|
+
const compression = compress({
|
|
71
|
+
sevenZipPath,
|
|
72
|
+
src: appDir,
|
|
73
|
+
dest: archiveFile,
|
|
74
|
+
format: '7z',
|
|
75
|
+
level: config.win.compressionLevel,
|
|
76
|
+
})
|
|
77
|
+
logger.info(
|
|
78
|
+
` [5/11] app-${arch}.7z ready (mx=${compression.level}, mmt=${compression.threads})`,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
logger.info(' [6/11] Preparing NSIS toolchain ...')
|
|
82
|
+
const { nsisDir, nsisResourcesDir } = await prepareNsisToolchain(config.projectDir, config.win)
|
|
83
|
+
|
|
84
|
+
logger.info(' [7/11] Building NSIS installer script ...')
|
|
85
|
+
const exeFilename = buildWindowsInstallerFilename(config)
|
|
86
|
+
const exePath = path.join(outputDir, exeFilename)
|
|
87
|
+
|
|
88
|
+
const nsisTemplatesDir = getNsisTemplatesDir()
|
|
89
|
+
const pluginArch = 'x86-unicode'
|
|
90
|
+
const nsisPluginsDir = path.join(nsisResourcesDir, 'plugins', pluginArch)
|
|
91
|
+
const nsisIncludeDir = path.join(nsisTemplatesDir, 'include')
|
|
92
|
+
|
|
93
|
+
const guid = generateGuid(config.appId)
|
|
94
|
+
const uninstallAppKey = guid.replace(/\\/g, ' - ')
|
|
95
|
+
const identity = resolveWindowsPackageIdentity(config)
|
|
96
|
+
const appPackageName = config.name.replace(/\//g, '\\')
|
|
97
|
+
const updaterCacheDirName = identity.updaterCacheDirName
|
|
98
|
+
|
|
99
|
+
const installerNsiPath = path.join(nsisTemplatesDir, 'installer.nsi')
|
|
100
|
+
const installerNsiScript = await readFile(installerNsiPath, 'utf8')
|
|
101
|
+
logger.info(' [8/11] Generating uninstaller ...')
|
|
102
|
+
|
|
103
|
+
const uninstallerBuilderExe = path.join(stagingDir, `__uninstaller_builder_${Date.now()}.exe`)
|
|
104
|
+
const uninstallerExePath = path.join(stagingDir, `${exeFilename}__uninstaller.exe`)
|
|
105
|
+
|
|
106
|
+
const definesUninstaller: Record<string, string | null> = {
|
|
107
|
+
APP_ID: config.appId,
|
|
108
|
+
APP_GUID: guid,
|
|
109
|
+
UNINSTALL_APP_KEY: uninstallAppKey,
|
|
110
|
+
PRODUCT_NAME: config.productName,
|
|
111
|
+
PRODUCT_FILENAME: identity.productFilename,
|
|
112
|
+
APP_FILENAME: identity.appFilename,
|
|
113
|
+
APP_DESCRIPTION: smarten(config.description || ''),
|
|
114
|
+
APP_PACKAGE_NAME: appPackageName,
|
|
115
|
+
VERSION: config.version,
|
|
116
|
+
PROJECT_DIR: config.projectDir,
|
|
117
|
+
BUILD_RESOURCES_DIR: path.join(config.projectDir, 'build'),
|
|
118
|
+
COMPRESSION_METHOD: '7z',
|
|
119
|
+
COMPRESS: 'auto',
|
|
120
|
+
ONE_CLICK: null,
|
|
121
|
+
RUN_AFTER_FINISH: null,
|
|
122
|
+
BUILD_UNINSTALLER: null,
|
|
123
|
+
UNINSTALLER_OUT_FILE: uninstallerExePath,
|
|
124
|
+
SHORTCUT_NAME: identity.shortcutName,
|
|
125
|
+
UNINSTALL_DISPLAY_NAME: `${config.productName} ${config.version}`,
|
|
126
|
+
APP_INSTALLER_STORE_FILE: `${updaterCacheDirName}\\installer.exe`,
|
|
127
|
+
...(identity.appFilename !== identity.productFilename
|
|
128
|
+
? { APP_PRODUCT_FILENAME: identity.productFilename }
|
|
129
|
+
: {}),
|
|
130
|
+
}
|
|
131
|
+
if (config.author) {
|
|
132
|
+
definesUninstaller.COMPANY_NAME = config.author
|
|
133
|
+
}
|
|
134
|
+
if (uninstallAppKey !== guid) {
|
|
135
|
+
definesUninstaller.UNINSTALL_REGISTRY_KEY_2 = `Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${guid}`
|
|
136
|
+
}
|
|
137
|
+
const archDefineKey = arch === Arch.ARM64 ? 'APP_ARM64' : 'APP_64'
|
|
138
|
+
definesUninstaller[archDefineKey] = archiveFile
|
|
139
|
+
|
|
140
|
+
const commandsUninstaller: Record<string, string | boolean | string[]> = {
|
|
141
|
+
OutFile: `"${uninstallerBuilderExe}"`,
|
|
142
|
+
VIProductVersion: getBuildVersion(config.version, config.buildNumber),
|
|
143
|
+
VIAddVersionKey: computeVersionKey(config),
|
|
144
|
+
Unicode: true,
|
|
145
|
+
SetCompressor: 'zlib',
|
|
146
|
+
}
|
|
147
|
+
if (iconPath) {
|
|
148
|
+
definesUninstaller.MUI_ICON = iconPath
|
|
149
|
+
definesUninstaller.MUI_UNICON = iconPath
|
|
150
|
+
}
|
|
151
|
+
const headerUninstaller = await buildNsisScriptHeader({
|
|
152
|
+
nsisIncludeDir,
|
|
153
|
+
nsisPluginsDir,
|
|
154
|
+
pluginArch,
|
|
155
|
+
nsisTemplatesDir,
|
|
156
|
+
stagingDir,
|
|
157
|
+
})
|
|
158
|
+
await executeMakensis({
|
|
159
|
+
nsisDir,
|
|
160
|
+
defines: definesUninstaller,
|
|
161
|
+
commands: commandsUninstaller,
|
|
162
|
+
script: headerUninstaller + installerNsiScript,
|
|
163
|
+
cwd: nsisTemplatesDir,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
if (!existsSync(uninstallerBuilderExe)) {
|
|
167
|
+
throw new Error(`Phase 1 installer not found: ${uninstallerBuilderExe}`)
|
|
168
|
+
}
|
|
169
|
+
const runResult = spawnSync(uninstallerBuilderExe, [], {
|
|
170
|
+
stdio: 'pipe',
|
|
171
|
+
windowsHide: true,
|
|
172
|
+
env: { __COMPAT_LAYER: 'RunAsInvoker' },
|
|
173
|
+
timeout: 30000,
|
|
174
|
+
})
|
|
175
|
+
if (runResult.status !== 0) {
|
|
176
|
+
console.warn(` Warning: Phase 1 installer exited with code ${runResult.status}`)
|
|
177
|
+
}
|
|
178
|
+
if (!existsSync(uninstallerExePath)) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Uninstaller not generated after running Phase 1 installer. Expected: ${uninstallerExePath}`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (existsSync(uninstallerBuilderExe)) {
|
|
185
|
+
await rm(uninstallerBuilderExe, { force: true })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (config.sign.enabled && config.sign.win?.enabled && config.sign.win?.hook) {
|
|
189
|
+
logger.info(' [9/11] Running sign hook on uninstaller ...')
|
|
190
|
+
await config.sign.win.hook(uninstallerExePath, { config, arch, outputDir, singleFile: true })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
logger.info(' [10/11] Compiling NSIS installer ...')
|
|
194
|
+
|
|
195
|
+
const { BUILD_UNINSTALLER: _omit, ...definesRest } = definesUninstaller
|
|
196
|
+
const defines: Record<string, string | null> = definesRest
|
|
197
|
+
defines.UNINSTALLER_OUT_FILE = uninstallerExePath
|
|
198
|
+
|
|
199
|
+
const commands = { ...commandsUninstaller }
|
|
200
|
+
commands.OutFile = `"${exePath}"`
|
|
201
|
+
|
|
202
|
+
const archiveHashHex = hashFile(archiveFile, 'sha512', 'hex').toUpperCase()
|
|
203
|
+
defines[`${archDefineKey}_NAME`] = path.basename(archiveFile)
|
|
204
|
+
defines[`${archDefineKey}_HASH`] = archiveHashHex
|
|
205
|
+
|
|
206
|
+
const archiveStat = await stat(archiveFile)
|
|
207
|
+
const estimatedUnpackedSize = Math.ceil((archiveStat.size * 3) / 1024).toString()
|
|
208
|
+
defines[`${archDefineKey}_UNPACKED_SIZE`] = estimatedUnpackedSize
|
|
209
|
+
|
|
210
|
+
const estimatedSize = computeEstimatedSize(sevenZipPath, archiveFile)
|
|
211
|
+
if (estimatedSize > 0) {
|
|
212
|
+
defines.ESTIMATED_SIZE = String(Math.round(estimatedSize / 1024))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const headerInstaller = await buildNsisScriptHeader({
|
|
216
|
+
nsisIncludeDir,
|
|
217
|
+
nsisPluginsDir,
|
|
218
|
+
pluginArch,
|
|
219
|
+
nsisTemplatesDir,
|
|
220
|
+
stagingDir,
|
|
221
|
+
})
|
|
222
|
+
await executeMakensis({
|
|
223
|
+
nsisDir,
|
|
224
|
+
defines,
|
|
225
|
+
commands,
|
|
226
|
+
script: headerInstaller + installerNsiScript,
|
|
227
|
+
cwd: nsisTemplatesDir,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
if (existsSync(uninstallerExePath)) {
|
|
231
|
+
await rm(uninstallerExePath, { force: true })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (config.sign.enabled && config.sign.win?.enabled && config.sign.win?.hook) {
|
|
235
|
+
logger.info(' [11/11] Running sign hook on installer ...')
|
|
236
|
+
await config.sign.win.hook(exePath, { config, arch, outputDir, singleFile: true })
|
|
237
|
+
} else {
|
|
238
|
+
logger.info(' [11/11] Signing disabled')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const blockmapPath = `${exePath}.blockmap`
|
|
242
|
+
const blockMapSize = (await buildBlockMap(exePath, 'gzip', blockmapPath)).blockMapSize
|
|
243
|
+
logger.info(` [Windows] ${arch} blockmap ready: ${blockmapPath}`)
|
|
244
|
+
|
|
245
|
+
if (config.update.generateMetadata) {
|
|
246
|
+
generateUpdateMetadata({
|
|
247
|
+
filePath: exePath,
|
|
248
|
+
version: config.version,
|
|
249
|
+
outputDir,
|
|
250
|
+
platform: NodePlatform.WIN32,
|
|
251
|
+
blockMapSize,
|
|
252
|
+
channel: config.update.channel,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
logger.info(` [Windows] ${arch} packaging complete: ${exePath}`)
|
|
257
|
+
return exePath
|
|
258
|
+
} finally {
|
|
259
|
+
await removeStagingDir(stagingDir)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Derive the installer .exe filename from the distribution naming contract. */
|
|
264
|
+
export function buildWindowsInstallerFilename(config: NormalizedConfig): string {
|
|
265
|
+
return `${config.artifactName}_${config.version}.exe`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Derive the Windows package identity (install dir, shortcut, updater cache). */
|
|
269
|
+
export function resolveWindowsPackageIdentity(config: NormalizedConfig): {
|
|
270
|
+
appFilename: string
|
|
271
|
+
productFilename: string
|
|
272
|
+
shortcutName: string
|
|
273
|
+
updaterCacheDirName: string
|
|
274
|
+
} {
|
|
275
|
+
return {
|
|
276
|
+
appFilename: getWindowsInstallationDirName(config),
|
|
277
|
+
productFilename: config.productName,
|
|
278
|
+
shortcutName: config.win.shortcutName || config.productName,
|
|
279
|
+
updaterCacheDirName: getUpdaterCacheDirName(config),
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function assembleAppDir({
|
|
284
|
+
entries,
|
|
285
|
+
appDir,
|
|
286
|
+
asarPath,
|
|
287
|
+
unpackedDir,
|
|
288
|
+
config,
|
|
289
|
+
arch,
|
|
290
|
+
}: {
|
|
291
|
+
entries: ZipEntry[]
|
|
292
|
+
appDir: string
|
|
293
|
+
asarPath: string
|
|
294
|
+
unpackedDir?: string
|
|
295
|
+
config: NormalizedConfig
|
|
296
|
+
arch: Arch
|
|
297
|
+
}): Promise<void> {
|
|
298
|
+
const productExeName = `${config.productName}.exe`
|
|
299
|
+
|
|
300
|
+
for (const e of entries) {
|
|
301
|
+
if (isDefaultAppEntry(e.path)) continue
|
|
302
|
+
|
|
303
|
+
if (e.dir) {
|
|
304
|
+
await mkdir(path.join(appDir, e.path), { recursive: true })
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let destPath = e.path
|
|
309
|
+
if (e.path === 'electron.exe') {
|
|
310
|
+
destPath = productExeName
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const fullPath = path.join(appDir, destPath)
|
|
314
|
+
const parentDir = path.dirname(fullPath)
|
|
315
|
+
if (!existsSync(parentDir)) {
|
|
316
|
+
await mkdir(parentDir, { recursive: true })
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const buffer = await getEntryBuffer(e.entry)
|
|
320
|
+
await writeFile(fullPath, buffer)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const asarDest = path.join(appDir, 'resources', 'app.asar')
|
|
324
|
+
if (existsSync(asarPath)) {
|
|
325
|
+
await cp(asarPath, asarDest)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (unpackedDir && existsSync(unpackedDir)) {
|
|
329
|
+
const unpackedDest = path.join(appDir, 'resources', 'app.asar.unpacked')
|
|
330
|
+
await cp(unpackedDir, unpackedDest, { recursive: true })
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await injectExtraResources(appDir, config, arch)
|
|
334
|
+
await writeAppUpdateYml(appDir, config)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function writeAppUpdateYml(appDir: string, config: NormalizedConfig): Promise<void> {
|
|
338
|
+
if (!config.update.url) return
|
|
339
|
+
|
|
340
|
+
const updateYml = await renderAppUpdateYml(config)
|
|
341
|
+
const destDir = path.join(appDir, 'resources')
|
|
342
|
+
await mkdir(destDir, { recursive: true })
|
|
343
|
+
await writeFile(path.join(destDir, 'app-update.yml'), updateYml, 'utf8')
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function injectExtraResources(
|
|
347
|
+
appDir: string,
|
|
348
|
+
config: NormalizedConfig,
|
|
349
|
+
_arch: Arch,
|
|
350
|
+
): Promise<void> {
|
|
351
|
+
for (const { res, srcPath, srcStat, destPath: resTo } of await resolveExtraResources(config)) {
|
|
352
|
+
const destPath = path.join(appDir, 'resources', resTo)
|
|
353
|
+
if (srcStat.isDirectory()) {
|
|
354
|
+
await cp(srcPath, destPath, {
|
|
355
|
+
recursive: true,
|
|
356
|
+
filter: (entryPath) => !isExtraResourcePathExcluded(res, path.relative(srcPath, entryPath)),
|
|
357
|
+
})
|
|
358
|
+
} else {
|
|
359
|
+
const destDir = path.dirname(destPath)
|
|
360
|
+
if (!existsSync(destDir)) await mkdir(destDir, { recursive: true })
|
|
361
|
+
await cp(srcPath, destPath)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Build the NSIS version-info keys (locale 1033) for the installer. */
|
|
367
|
+
function computeVersionKey(config: NormalizedConfig): string[] {
|
|
368
|
+
const localeId = '1033'
|
|
369
|
+
const keys = [
|
|
370
|
+
`/LANG=${localeId} ProductName "${config.productName}"`,
|
|
371
|
+
`/LANG=${localeId} ProductVersion "${config.version}"`,
|
|
372
|
+
`/LANG=${localeId} LegalCopyright "${config.copyright}"`,
|
|
373
|
+
`/LANG=${localeId} FileDescription "${config.description || ''}"`,
|
|
374
|
+
`/LANG=${localeId} FileVersion "${config.version}"`,
|
|
375
|
+
]
|
|
376
|
+
if (config.author) {
|
|
377
|
+
keys.push(`/LANG=${localeId} CompanyName "${config.author}"`)
|
|
378
|
+
}
|
|
379
|
+
return keys
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Derive a stable v5 UUID (sha1 name-based) from the appId. */
|
|
383
|
+
function generateGuid(appId: string): string {
|
|
384
|
+
const NAMESPACE = Buffer.from([
|
|
385
|
+
0x50, 0xe0, 0x65, 0xbc, 0x31, 0x34, 0x11, 0xe6, 0x9b, 0xab, 0x38, 0xc9, 0x86, 0x2b, 0xda, 0xf3,
|
|
386
|
+
])
|
|
387
|
+
const hash = crypto.createHash('sha1')
|
|
388
|
+
hash.update(NAMESPACE)
|
|
389
|
+
hash.update(appId)
|
|
390
|
+
const buffer = hash.digest()
|
|
391
|
+
const byte2hex: string[] = []
|
|
392
|
+
for (let i = 0; i < 256; i++) {
|
|
393
|
+
byte2hex[i] = (i + 0x100).toString(16).substring(1)
|
|
394
|
+
}
|
|
395
|
+
// Map a byte offset to its lowercase hex digit.
|
|
396
|
+
const h = (i: number): string => byte2hex[buffer[i]]
|
|
397
|
+
return (
|
|
398
|
+
`${h(0)}${h(1)}${h(2)}${h(3)}-` +
|
|
399
|
+
`${h(4)}${h(5)}-` +
|
|
400
|
+
`${byte2hex[(buffer[6] & 0x0f) | 0x50]}${h(7)}-` +
|
|
401
|
+
`${byte2hex[(buffer[8] & 0x3f) | 0x80]}${h(9)}-` +
|
|
402
|
+
`${h(10)}${h(11)}${h(12)}${h(13)}${h(14)}${h(15)}`
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Derive a filesystem-safe installation directory name. */
|
|
407
|
+
function getWindowsInstallationDirName(config: NormalizedConfig): string {
|
|
408
|
+
const productFilename = config.productName
|
|
409
|
+
if (/^[-_+0-9a-zA-Z .]+$/.test(productFilename)) {
|
|
410
|
+
return productFilename
|
|
411
|
+
}
|
|
412
|
+
return config.name
|
|
413
|
+
.replace(/@/g, '')
|
|
414
|
+
.replace(/\//g, '-')
|
|
415
|
+
.replace(/[^-._+0-9a-zA-Z ]/g, '')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Replace ASCII quotes with a typographic quote for NSIS strings. */
|
|
419
|
+
function smarten(str: string): string {
|
|
420
|
+
return str.replace(/"/g, '”')
|
|
421
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main packaging pipeline: validate, collect the payload, then pack each target.
|
|
3
|
+
*
|
|
4
|
+
* Targets run concurrently; a target failure is captured and reported
|
|
5
|
+
* without aborting the remaining targets.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdir } from 'node:fs/promises'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { setPrebuiltDirResolver } from '../asar/fileset/platform-fileset.ts'
|
|
10
|
+
import { collectProjectFiles, packAsarForTarget } from '../asar/pack.ts'
|
|
11
|
+
import type { NormalizedConfig } from '../config/index.ts'
|
|
12
|
+
import { removeStagingDir } from '../packer/common/staging.ts'
|
|
13
|
+
import { createLogger } from '../shared/logger.ts'
|
|
14
|
+
import type { Arch, Platform } from '../shared/platform.ts'
|
|
15
|
+
import type { FileSet } from '../shared/types.ts'
|
|
16
|
+
import { type PackTarget, runPlatformPackers } from './targets.ts'
|
|
17
|
+
|
|
18
|
+
export interface PackResult {
|
|
19
|
+
ok: boolean
|
|
20
|
+
outputDir: string
|
|
21
|
+
targets: TargetResult[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PipelineOptions {
|
|
25
|
+
platforms: Platform[]
|
|
26
|
+
arches: Arch[]
|
|
27
|
+
output: string
|
|
28
|
+
dryRun: boolean
|
|
29
|
+
onTargetComplete?: (result: TargetResult) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TargetResult {
|
|
33
|
+
platform: Platform
|
|
34
|
+
arch: Arch
|
|
35
|
+
output?: string
|
|
36
|
+
error?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TargetSpec {
|
|
40
|
+
platform: Platform
|
|
41
|
+
arches?: Arch[]
|
|
42
|
+
config?: (config: NormalizedConfig) => NormalizedConfig
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface CollectedProjectFiles {
|
|
46
|
+
appFileSet: FileSet
|
|
47
|
+
nmFileSets: FileSet[]
|
|
48
|
+
transformer: (file: string) => Promise<string | null> | string | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface SharedState {
|
|
52
|
+
collectPromise?: Promise<CollectedProjectFiles>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function resolveAsarForTarget(
|
|
56
|
+
target: PackTarget,
|
|
57
|
+
config: NormalizedConfig,
|
|
58
|
+
outputDir: string,
|
|
59
|
+
shared: SharedState,
|
|
60
|
+
) {
|
|
61
|
+
if (config.asar?.path) {
|
|
62
|
+
return {
|
|
63
|
+
asarPath: path.resolve(config.projectDir, config.asar.path),
|
|
64
|
+
unpackedDir: config.asar.unpackedDir
|
|
65
|
+
? path.resolve(config.projectDir, config.asar.unpackedDir)
|
|
66
|
+
: undefined,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!shared.collectPromise) {
|
|
71
|
+
shared.collectPromise = collectProjectFiles(config.projectDir, config)
|
|
72
|
+
}
|
|
73
|
+
const { appFileSet, nmFileSets, transformer } = await shared.collectPromise
|
|
74
|
+
|
|
75
|
+
const stagingDir = path.join(outputDir, 'cross-packer-asar-staging')
|
|
76
|
+
await mkdir(stagingDir, { recursive: true })
|
|
77
|
+
return packAsarForTarget({
|
|
78
|
+
platform: target.platform,
|
|
79
|
+
arch: target.arch,
|
|
80
|
+
appFileSet,
|
|
81
|
+
nmFileSets: nmFileSets ?? [],
|
|
82
|
+
transformer: transformer ?? (() => null),
|
|
83
|
+
config,
|
|
84
|
+
projectDir: config.projectDir,
|
|
85
|
+
stagingDir,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function runPack(
|
|
90
|
+
config: NormalizedConfig,
|
|
91
|
+
options: PipelineOptions,
|
|
92
|
+
targetSpecs?: TargetSpec[],
|
|
93
|
+
): Promise<PackResult> {
|
|
94
|
+
const { dryRun } = options
|
|
95
|
+
const logger = createLogger()
|
|
96
|
+
|
|
97
|
+
setPrebuiltDirResolver(config.nativeModules?.prebuiltRules ?? null)
|
|
98
|
+
|
|
99
|
+
const outputDir = resolveOutputDir(config, options)
|
|
100
|
+
await mkdir(outputDir, { recursive: true })
|
|
101
|
+
const results: TargetResult[] = []
|
|
102
|
+
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
logger.info('Dry run: config validated, targets resolved, no build performed.')
|
|
105
|
+
for (const platform of options.platforms) {
|
|
106
|
+
for (const arch of options.arches) {
|
|
107
|
+
const entry = { platform, arch, output: '(dry-run)' }
|
|
108
|
+
results.push(entry)
|
|
109
|
+
options.onTargetComplete?.(entry)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, outputDir, targets: results }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const targets = runPlatformPackers({ config, options, outputDir, targetSpecs })
|
|
116
|
+
|
|
117
|
+
// State shared across targets: project file collection runs only once.
|
|
118
|
+
const shared: SharedState = {}
|
|
119
|
+
|
|
120
|
+
const settled = await Promise.all(
|
|
121
|
+
targets.map(async (target: PackTarget) => {
|
|
122
|
+
try {
|
|
123
|
+
const asar = await resolveAsarForTarget(target, config, outputDir, shared)
|
|
124
|
+
const output = await target.run(asar)
|
|
125
|
+
return { platform: target.platform, arch: target.arch, output }
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
128
|
+
logger.error(`${target.platform}-${target.arch}: ${message}`)
|
|
129
|
+
return { platform: target.platform, arch: target.arch, error: message }
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
for (const entry of settled) {
|
|
135
|
+
options.onTargetComplete?.(entry)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const stagingDir = path.join(outputDir, 'cross-packer-asar-staging')
|
|
139
|
+
await removeStagingDir(stagingDir)
|
|
140
|
+
|
|
141
|
+
results.push(...settled)
|
|
142
|
+
return { ok: !results.some((r) => r.error), outputDir, targets: results }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveOutputDir(config: NormalizedConfig, options: PipelineOptions): string {
|
|
146
|
+
if (options.output) return path.resolve(options.output)
|
|
147
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
148
|
+
return path.join(config.projectDir, 'dist', `cross-packer-${stamp}`)
|
|
149
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target resolution: maps (platform, arch) pairs to packer implementations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NormalizedConfig } from '../config/index.ts'
|
|
6
|
+
import { packLinux } from '../packer/linux/linux.ts'
|
|
7
|
+
import { packMac } from '../packer/mac/mac.ts'
|
|
8
|
+
import { packWin } from '../packer/win/win.ts'
|
|
9
|
+
import type { Arch, Platform } from '../shared/platform.ts'
|
|
10
|
+
import type { PipelineOptions, TargetSpec } from './pipeline.ts'
|
|
11
|
+
|
|
12
|
+
const PACKERS: Record<Platform, (ctx: PackerContext) => Promise<string>> = {
|
|
13
|
+
win: packWin,
|
|
14
|
+
mac: packMac,
|
|
15
|
+
linux: packLinux,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PackerContext {
|
|
19
|
+
config: NormalizedConfig
|
|
20
|
+
arch: Arch
|
|
21
|
+
outputDir: string
|
|
22
|
+
asarPath?: string
|
|
23
|
+
unpackedDir?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A single (platform, arch) packaging unit. */
|
|
27
|
+
export interface PackTarget {
|
|
28
|
+
platform: Platform
|
|
29
|
+
arch: Arch
|
|
30
|
+
run: (asar: { asarPath?: string; unpackedDir?: string }) => Promise<string>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function runPlatformPackers({
|
|
34
|
+
config,
|
|
35
|
+
options,
|
|
36
|
+
outputDir,
|
|
37
|
+
targetSpecs,
|
|
38
|
+
}: {
|
|
39
|
+
config: NormalizedConfig
|
|
40
|
+
options: PipelineOptions
|
|
41
|
+
outputDir: string
|
|
42
|
+
targetSpecs?: TargetSpec[]
|
|
43
|
+
}) {
|
|
44
|
+
const specs: TargetSpec[] =
|
|
45
|
+
targetSpecs && targetSpecs.length > 0
|
|
46
|
+
? targetSpecs
|
|
47
|
+
: options.platforms.map((platform) => ({ platform, arches: options.arches }))
|
|
48
|
+
|
|
49
|
+
const targets: PackTarget[] = []
|
|
50
|
+
for (const spec of specs) {
|
|
51
|
+
const packer = PACKERS[spec.platform]
|
|
52
|
+
if (!packer) {
|
|
53
|
+
throw new Error(`No packer registered for platform "${spec.platform}"`)
|
|
54
|
+
}
|
|
55
|
+
const arches = spec.arches ?? options.arches
|
|
56
|
+
const targetConfig = spec.config ? spec.config(config) : config
|
|
57
|
+
for (const arch of arches) {
|
|
58
|
+
targets.push({
|
|
59
|
+
platform: spec.platform,
|
|
60
|
+
arch,
|
|
61
|
+
run: (asar: { asarPath?: string; unpackedDir?: string }) =>
|
|
62
|
+
packer({ config: targetConfig, arch, outputDir, ...asar }),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return targets
|
|
67
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File hashing helpers shared by update metadata, toolchain verification,
|
|
3
|
+
* and archive hashing. Zero project dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
/** Hash a file's contents with the given algorithm and digest encoding. */
|
|
9
|
+
export function hashFile(
|
|
10
|
+
filePath: string,
|
|
11
|
+
algorithm: 'sha256' | 'sha512' = 'sha512',
|
|
12
|
+
encoding: 'hex' | 'base64' = 'hex',
|
|
13
|
+
): string {
|
|
14
|
+
return createHash(algorithm).update(readFileSync(filePath)).digest(encoding)
|
|
15
|
+
}
|