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,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
|
+
}
|