Skip to content

Commit 866b0ca

Browse files
authored
fix: use FileCopier for copying files and queue creation of symlinks (#8663)
1 parent 88cc0b0 commit 866b0ca

File tree

28 files changed

+752
-59
lines changed

28 files changed

+752
-59
lines changed

packages/app-builder-lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"dependencies": {
4949
"@develar/schema-utils": "~2.6.5",
5050
"@electron/fuses": "^1.8.0",
51-
"@electron/asar": "^3.2.13",
51+
"@electron/asar": "3.2.13",
5252
"@electron/notarize": "2.5.0",
5353
"@electron/osx-sign": "1.3.1",
5454
"@electron/rebuild": "3.7.0",

packages/app-builder-lib/src/asar/asarUtil.ts

Lines changed: 68 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { CreateOptions, createPackageWithOptions } from "@electron/asar"
22
import { AsyncTaskManager, log } from "builder-util"
33
import { CancellationToken } from "builder-util-runtime"
4-
import { Filter, MAX_FILE_REQUESTS } from "builder-util/out/fs"
5-
import * as fsNode from "fs"
4+
import { FileCopier, Filter, Link, MAX_FILE_REQUESTS } from "builder-util/out/fs"
65
import * as fs from "fs-extra"
6+
import { mkdir, readlink, symlink } from "fs-extra"
7+
import { platform } from "os"
78
import * as path from "path"
89
import * as tempFile from "temp-file"
910
import { AsarOptions } from "../options/PlatformSpecificBuildOptions"
@@ -15,9 +16,12 @@ import { detectUnpackedDirs } from "./unpackDetector"
1516
export class AsarPackager {
1617
private readonly outFile: string
1718
private rootForAppFilesWithoutAsar!: string
18-
private readonly tmpDir = new tempFile.TmpDir()
19+
private readonly fileCopier = new FileCopier()
20+
private readonly tmpDir: tempFile.TmpDir
21+
private readonly cancellationToken: CancellationToken
1922

2023
constructor(
24+
readonly packager: PlatformPackager<any>,
2125
private readonly config: {
2226
defaultDestination: string
2327
resourcePath: string
@@ -26,14 +30,13 @@ export class AsarPackager {
2630
}
2731
) {
2832
this.outFile = path.join(config.resourcePath, `app.asar`)
33+
this.tmpDir = packager.info.tempDirManager
34+
this.cancellationToken = packager.info.cancellationToken
2935
}
3036

31-
async pack(fileSets: Array<ResolvedFileSet>, _packager: PlatformPackager<any>) {
37+
async pack(fileSets: Array<ResolvedFileSet>) {
3238
this.rootForAppFilesWithoutAsar = await this.tmpDir.getTempDir({ prefix: "asar-app" })
3339

34-
const cancellationToken = new CancellationToken()
35-
cancellationToken.on("cancel", () => this.tmpDir.cleanupSync())
36-
3740
const orderedFileSets = [
3841
// Write dependencies first to minimize offset changes to asar header
3942
...fileSets.slice(1),
@@ -42,9 +45,13 @@ export class AsarPackager {
4245
fileSets[0],
4346
].map(orderFileSet)
4447

45-
const { unpackedPaths, copiedFiles } = await this.detectAndCopy(orderedFileSets, cancellationToken)
48+
const { unpackedPaths, copiedFiles } = await this.detectAndCopy(orderedFileSets)
4649
const unpackGlob = unpackedPaths.length > 1 ? `{${unpackedPaths.join(",")}}` : unpackedPaths.pop()
4750

51+
await this.executeElectronAsar(copiedFiles, unpackGlob)
52+
}
53+
54+
private async executeElectronAsar(copiedFiles: string[], unpackGlob: string | undefined) {
4855
let ordering = this.config.options.ordering || undefined
4956
if (!ordering) {
5057
// `copiedFiles` are already ordered due to `orderedFileSets` input, so we just map to their relative paths (via substring) within the asar.
@@ -69,95 +76,106 @@ export class AsarPackager {
6976
}
7077
await createPackageWithOptions(this.rootForAppFilesWithoutAsar, this.outFile, options)
7178
console.log = consoleLogger
72-
73-
await this.tmpDir.cleanup()
7479
}
7580

76-
private async detectAndCopy(fileSets: ResolvedFileSet[], cancellationToken: CancellationToken) {
77-
const taskManager = new AsyncTaskManager(cancellationToken)
81+
private async detectAndCopy(fileSets: ResolvedFileSet[]) {
82+
const taskManager = new AsyncTaskManager(this.cancellationToken)
7883
const unpackedPaths = new Set<string>()
7984
const copiedFiles = new Set<string>()
8085

86+
const createdSourceDirs = new Set<string>()
87+
const links: Array<Link> = []
88+
const symlinkType = platform() === "win32" ? "junction" : "file"
89+
8190
const matchUnpacker = (file: string, dest: string, stat: fs.Stats) => {
8291
if (this.config.unpackPattern?.(file, stat)) {
8392
log.debug({ file }, "unpacking")
8493
unpackedPaths.add(dest)
8594
return
8695
}
8796
}
88-
const writeFileOrSymlink = async (options: { transformedData: string | Buffer | undefined; file: string; destination: string; stat: fs.Stats; fileSet: ResolvedFileSet }) => {
89-
const {
90-
transformedData,
91-
file: source,
92-
destination,
93-
stat,
94-
fileSet: { src: sourceDir },
95-
} = options
97+
const writeFileOrProcessSymlink = async (options: {
98+
file: string
99+
destination: string
100+
stat: fs.Stats
101+
fileSet: ResolvedFileSet
102+
transformedData: string | Buffer | undefined
103+
}) => {
104+
const { transformedData, file, destination, stat, fileSet } = options
105+
if (!stat.isFile() && !stat.isSymbolicLink()) {
106+
return
107+
}
96108
copiedFiles.add(destination)
97109

98-
// If transformed data, skip symlink logic
99-
if (transformedData) {
100-
return this.copyFileOrData(transformedData, source, destination, stat)
110+
const dir = path.dirname(destination)
111+
if (!createdSourceDirs.has(dir)) {
112+
await mkdir(dir, { recursive: true })
113+
createdSourceDirs.add(dir)
101114
}
102115

103-
const realPathFile = await fs.realpath(source)
104-
105-
if (source === realPathFile) {
106-
return this.copyFileOrData(undefined, source, destination, stat)
116+
// write any data if provided, skip symlink check
117+
if (transformedData != null) {
118+
return fs.writeFile(destination, transformedData, { mode: stat.mode })
107119
}
108120

109-
const realPathRelative = path.relative(sourceDir, realPathFile)
121+
const realPathFile = await fs.realpath(file)
122+
const realPathRelative = path.relative(fileSet.src, realPathFile)
110123
const isOutsidePackage = realPathRelative.startsWith("..")
111124
if (isOutsidePackage) {
112-
log.error({ source: log.filePath(source), realPathFile: log.filePath(realPathFile) }, `unable to copy, file is symlinked outside the package`)
113-
throw new Error(
114-
`Cannot copy file (${path.basename(source)}) symlinked to file (${path.basename(realPathFile)}) outside the package as that violates asar security integrity`
115-
)
125+
log.error({ source: log.filePath(file), realPathFile: log.filePath(realPathFile) }, `unable to copy, file is symlinked outside the package`)
126+
throw new Error(`Cannot copy file (${path.basename(file)}) symlinked to file (${path.basename(realPathFile)}) outside the package as that violates asar security integrity`)
116127
}
117128

118-
const symlinkTarget = path.resolve(this.rootForAppFilesWithoutAsar, realPathRelative)
119-
await this.copyFileOrData(undefined, source, symlinkTarget, stat)
120-
const target = path.relative(path.dirname(destination), symlinkTarget)
121-
fsNode.symlinkSync(target, destination)
129+
// not a symlink, copy directly
130+
if (file === realPathFile) {
131+
return this.fileCopier.copy(file, destination, stat)
132+
}
122133

123-
copiedFiles.add(symlinkTarget)
134+
// okay, it must be a symlink. evaluate link to be relative to source file in asar
135+
let link = await readlink(file)
136+
if (path.isAbsolute(link)) {
137+
link = path.relative(path.dirname(file), link)
138+
}
139+
links.push({ file: destination, link })
124140
}
125141

126142
for await (const fileSet of fileSets) {
127143
if (this.config.options.smartUnpack !== false) {
128144
detectUnpackedDirs(fileSet, unpackedPaths, this.config.defaultDestination)
129145
}
146+
147+
// Don't use BluebirdPromise, we need to retain order of execution/iteration through the ordered fileset
130148
for (let i = 0; i < fileSet.files.length; i++) {
131149
const file = fileSet.files[i]
132150
const transformedData = fileSet.transformedFiles?.get(i)
133-
const metadata = fileSet.metadata.get(file) || (await fs.lstat(file))
151+
const stat = fileSet.metadata.get(file)!
134152

135153
const relative = path.relative(this.config.defaultDestination, getDestinationPath(file, fileSet))
136-
const dest = path.resolve(this.rootForAppFilesWithoutAsar, relative)
154+
const destination = path.resolve(this.rootForAppFilesWithoutAsar, relative)
137155

138-
matchUnpacker(file, dest, metadata)
139-
taskManager.addTask(writeFileOrSymlink({ transformedData, file, destination: dest, stat: metadata, fileSet }))
156+
matchUnpacker(file, destination, stat)
157+
taskManager.addTask(writeFileOrProcessSymlink({ transformedData, file, destination, stat, fileSet }))
140158

141159
if (taskManager.tasks.length > MAX_FILE_REQUESTS) {
142160
await taskManager.awaitTasks()
143161
}
144162
}
145163
}
164+
// finish copy then set up all symlinks
165+
await taskManager.awaitTasks()
166+
for (const it of links) {
167+
taskManager.addTask(symlink(it.link, it.file, symlinkType))
168+
169+
if (taskManager.tasks.length > MAX_FILE_REQUESTS) {
170+
await taskManager.awaitTasks()
171+
}
172+
}
146173
await taskManager.awaitTasks()
147174
return {
148175
unpackedPaths: Array.from(unpackedPaths),
149176
copiedFiles: Array.from(copiedFiles),
150177
}
151178
}
152-
153-
private async copyFileOrData(data: string | Buffer | undefined, source: string, destination: string, stat: fs.Stats) {
154-
await fs.mkdir(path.dirname(destination), { recursive: true })
155-
if (data) {
156-
await fs.writeFile(destination, data, { mode: stat.mode })
157-
} else {
158-
await fs.copyFile(source, destination)
159-
}
160-
}
161179
}
162180

163181
function orderFileSet(fileSet: ResolvedFileSet): ResolvedFileSet {

packages/app-builder-lib/src/platformPackager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,12 +504,12 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
504504
await transformFiles(transformer, fileSet)
505505
}
506506

507-
await new AsarPackager({
507+
await new AsarPackager(this, {
508508
defaultDestination,
509509
resourcePath,
510510
options: asarOptions,
511511
unpackPattern: fileMatcher?.createFilter(),
512-
}).pack(fileSets, this)
512+
}).pack(fileSets)
513513
})
514514
)
515515
}

pnpm-lock.yaml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)