11import { CreateOptions , createPackageWithOptions } from "@electron/asar"
22import { AsyncTaskManager , log } from "builder-util"
33import { 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"
65import * as fs from "fs-extra"
6+ import { mkdir , readlink , symlink } from "fs-extra"
7+ import { platform } from "os"
78import * as path from "path"
89import * as tempFile from "temp-file"
910import { AsarOptions } from "../options/PlatformSpecificBuildOptions"
@@ -15,9 +16,12 @@ import { detectUnpackedDirs } from "./unpackDetector"
1516export 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
163181function orderFileSet ( fileSet : ResolvedFileSet ) : ResolvedFileSet {
0 commit comments