-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
Copy pathasarUtil.ts
232 lines (199 loc) · 8.21 KB
/
asarUtil.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import { CreateOptions, createPackageWithOptions } from "@electron/asar"
import { AsyncTaskManager, log } from "builder-util"
import { CancellationToken } from "builder-util-runtime"
import { FileCopier, Filter, Link, MAX_FILE_REQUESTS } from "builder-util/out/fs"
import * as fs from "fs-extra"
import { mkdir, readlink, symlink } from "fs-extra"
import { platform } from "os"
import * as path from "path"
import * as tempFile from "temp-file"
import { AsarOptions } from "../options/PlatformSpecificBuildOptions"
import { PlatformPackager } from "../platformPackager"
import { ResolvedFileSet, getDestinationPath } from "../util/appFileCopier"
import { detectUnpackedDirs } from "./unpackDetector"
/** @internal */
export class AsarPackager {
private readonly outFile: string
private rootForAppFilesWithoutAsar!: string
private readonly fileCopier = new FileCopier()
private readonly tmpDir: tempFile.TmpDir
private readonly cancellationToken: CancellationToken
constructor(
readonly packager: PlatformPackager<any>,
private readonly config: {
defaultDestination: string
resourcePath: string
options: AsarOptions
unpackPattern: Filter | undefined
}
) {
this.outFile = path.join(config.resourcePath, `app.asar`)
this.tmpDir = packager.info.tempDirManager
this.cancellationToken = packager.info.cancellationToken
}
async pack(fileSets: Array<ResolvedFileSet>) {
this.rootForAppFilesWithoutAsar = await this.tmpDir.getTempDir({ prefix: "asar-app" })
const orderedFileSets = [
// Write dependencies first to minimize offset changes to asar header
...fileSets.slice(1),
// Finish with the app files that change most often
fileSets[0],
].map(orderFileSet)
const { unpackedPaths, copiedFiles } = await this.detectAndCopy(orderedFileSets)
const unpackGlob = unpackedPaths.length > 1 ? `{${unpackedPaths.join(",")}}` : unpackedPaths.pop()
await this.executeElectronAsar(copiedFiles, unpackGlob)
}
private async executeElectronAsar(copiedFiles: string[], unpackGlob: string | undefined) {
let ordering = this.config.options.ordering || undefined
if (!ordering) {
// `copiedFiles` are already ordered due to `orderedFileSets` input, so we just map to their relative paths (via substring) within the asar.
const filesSorted = copiedFiles.map(file => file.substring(this.rootForAppFilesWithoutAsar.length))
ordering = await this.tmpDir.getTempFile({ prefix: "asar-ordering", suffix: ".txt" })
await fs.writeFile(ordering, filesSorted.join("\n"))
}
const options: CreateOptions = {
unpack: unpackGlob,
unpackDir: unpackGlob,
ordering,
dot: true,
}
// override logger temporarily to clean up console (electron/asar does some internal logging that blogs up the default electron-builder logs)
const consoleLogger = console.log
console.log = (...args) => {
if (args[0] === "Ordering file has 100% coverage.") {
return // no need to log, this means our ordering logic is working correctly
}
log.info({ args }, "logging @electron/asar")
}
await createPackageWithOptions(this.rootForAppFilesWithoutAsar, this.outFile, options)
console.log = consoleLogger
}
private async detectAndCopy(fileSets: ResolvedFileSet[]) {
const taskManager = new AsyncTaskManager(this.cancellationToken)
const unpackedPaths = new Set<string>()
const copiedFiles = new Set<string>()
const createdSourceDirs = new Set<string>()
const links: Array<Link> = []
const symlinkType = platform() === "win32" ? "junction" : "file"
const matchUnpacker = (file: string, dest: string, stat: fs.Stats) => {
if (this.config.unpackPattern?.(file, stat)) {
log.debug({ file }, "unpacking")
unpackedPaths.add(dest)
return
}
}
const writeFileOrProcessSymlink = async (options: {
file: string
destination: string
stat: fs.Stats
fileSet: ResolvedFileSet
transformedData: string | Buffer | undefined
}) => {
const { transformedData, file, destination, stat, fileSet } = options
if (!stat.isFile() && !stat.isSymbolicLink()) {
return
}
copiedFiles.add(destination)
const dir = path.dirname(destination)
if (!createdSourceDirs.has(dir)) {
await mkdir(dir, { recursive: true })
createdSourceDirs.add(dir)
}
// write any data if provided, skip symlink check
if (transformedData != null) {
return fs.writeFile(destination, transformedData, { mode: stat.mode })
}
const realPathFile = await fs.realpath(file)
const realPathRelative = path.relative(fileSet.src, realPathFile)
const isOutsidePackage = realPathRelative.startsWith("..")
if (isOutsidePackage) {
log.error({ source: log.filePath(file), realPathFile: log.filePath(realPathFile) }, `unable to copy, file is symlinked outside the package`)
throw new Error(`Cannot copy file (${path.basename(file)}) symlinked to file (${path.basename(realPathFile)}) outside the package as that violates asar security integrity`)
}
// not a symlink, copy directly
if (file === realPathFile) {
return this.fileCopier.copy(file, destination, stat)
}
// okay, it must be a symlink. evaluate link to be relative to source file in asar
let link = await readlink(file)
if (path.isAbsolute(link)) {
link = path.relative(path.dirname(file), link)
}
links.push({ file: destination, link })
}
for await (const fileSet of fileSets) {
if (this.config.options.smartUnpack !== false) {
detectUnpackedDirs(fileSet, unpackedPaths, this.config.defaultDestination)
}
// Don't use BluebirdPromise, we need to retain order of execution/iteration through the ordered fileset
for (let i = 0; i < fileSet.files.length; i++) {
const file = fileSet.files[i]
const transformedData = fileSet.transformedFiles?.get(i)
const stat = fileSet.metadata.get(file)!
const relative = path.relative(this.config.defaultDestination, getDestinationPath(file, fileSet))
const destination = path.resolve(this.rootForAppFilesWithoutAsar, relative)
matchUnpacker(file, destination, stat)
taskManager.addTask(writeFileOrProcessSymlink({ transformedData, file, destination, stat, fileSet }))
if (taskManager.tasks.length > MAX_FILE_REQUESTS) {
await taskManager.awaitTasks()
}
}
}
// finish copy then set up all symlinks
await taskManager.awaitTasks()
for (const it of links) {
taskManager.addTask(symlink(it.link, it.file, symlinkType))
if (taskManager.tasks.length > MAX_FILE_REQUESTS) {
await taskManager.awaitTasks()
}
}
await taskManager.awaitTasks()
return {
unpackedPaths: Array.from(unpackedPaths),
copiedFiles: Array.from(copiedFiles),
}
}
}
function orderFileSet(fileSet: ResolvedFileSet): ResolvedFileSet {
const sortedFileEntries = Array.from(fileSet.files.entries())
sortedFileEntries.sort(([, a], [, b]) => {
if (a === b) {
return 0
}
// Place addons last because their signature changes per build
const isAAddon = a.endsWith(".node")
const isBAddon = b.endsWith(".node")
if (isAAddon && !isBAddon) {
return 1
}
if (isBAddon && !isAAddon) {
return -1
}
// Otherwise order by name
return a < b ? -1 : 1
})
let transformedFiles: Map<number, string | Buffer> | undefined
if (fileSet.transformedFiles) {
transformedFiles = new Map()
const indexMap = new Map<number, number>()
for (const [newIndex, [oldIndex]] of sortedFileEntries.entries()) {
indexMap.set(oldIndex, newIndex)
}
for (const [oldIndex, value] of fileSet.transformedFiles) {
const newIndex = indexMap.get(oldIndex)
if (newIndex === undefined) {
const file = fileSet.files[oldIndex]
throw new Error(`Internal error: ${file} was lost while ordering asar`)
}
transformedFiles.set(newIndex, value)
}
}
const { src, destination, metadata } = fileSet
return {
src,
destination,
metadata,
files: sortedFileEntries.map(([, file]) => file),
transformedFiles,
}
}