@@ -6,7 +6,7 @@ import events from 'events';
6
6
import * as crypto from 'crypto' ;
7
7
import type * as stream from 'stream' ;
8
8
import * as tar from 'tar' ;
9
- import { FileSystem , Path , ITerminal , FolderItem } from '@rushstack/node-core-library' ;
9
+ import { Async , FileSystem , Path , ITerminal , FolderItem } from '@rushstack/node-core-library' ;
10
10
11
11
import { RushConfigurationProject } from '../../api/RushConfigurationProject' ;
12
12
import { ProjectChangeAnalyzer } from '../ProjectChangeAnalyzer' ;
@@ -268,16 +268,25 @@ export class ProjectBuildCache {
268
268
269
269
const tarUtility : TarExecutable | undefined = await ProjectBuildCache . _tryGetTarUtility ( terminal ) ;
270
270
if ( tarUtility ) {
271
- const tempLocalCacheEntryPath : string = this . _localBuildCacheProvider . getCacheEntryPath ( cacheId ) ;
271
+ const finalLocalCacheEntryPath : string = this . _localBuildCacheProvider . getCacheEntryPath ( cacheId ) ;
272
+ // Derive the temp file from the destination path to ensure they are on the same volume
273
+ const tempLocalCacheEntryPath : string = `${ finalLocalCacheEntryPath } .temp` ;
272
274
const logFilePath : string = this . _getTarLogFilePath ( ) ;
273
275
const tarExitCode : number = await tarUtility . tryCreateArchiveFromProjectPathsAsync ( {
274
276
archivePath : tempLocalCacheEntryPath ,
275
277
paths : filesToCache . outputFilePaths ,
276
278
project : this . _project ,
277
279
logFilePath
278
280
} ) ;
281
+
279
282
if ( tarExitCode === 0 ) {
280
- localCacheEntryPath = tempLocalCacheEntryPath ;
283
+ // Move after the archive is finished so that if the process is interrupted we aren't left with an invalid file
284
+ await FileSystem . moveAsync ( {
285
+ sourcePath : tempLocalCacheEntryPath ,
286
+ destinationPath : finalLocalCacheEntryPath ,
287
+ overwrite : true
288
+ } ) ;
289
+ localCacheEntryPath = finalLocalCacheEntryPath ;
281
290
} else {
282
291
terminal . writeWarningLine (
283
292
`"tar" exited with code ${ tarExitCode } while attempting to create the cache entry. ` +
@@ -358,80 +367,82 @@ export class ProjectBuildCache {
358
367
return success ;
359
368
}
360
369
370
+ /**
371
+ * Walks the declared output folders of the project and collects a list of files.
372
+ * @returns The list of output files as project-relative paths, or `undefined` if a
373
+ * symbolic link was encountered.
374
+ */
361
375
private async _tryCollectPathsToCacheAsync ( terminal : ITerminal ) : Promise < IPathsToCache | undefined > {
362
- const projectFolderPath : string = this . _project . projectFolder ;
363
- const outputFolderNamesThatExist : boolean [ ] = await Promise . all (
364
- this . _projectOutputFolderNames . map ( ( outputFolderName ) =>
365
- FileSystem . existsAsync ( `${ projectFolderPath } /${ outputFolderName } ` )
366
- )
367
- ) ;
376
+ const posixPrefix : string = this . _project . projectFolder ;
377
+ const outputFilePaths : string [ ] = [ ] ;
378
+ const queue : [ string , string ] [ ] = [ ] ;
379
+
368
380
const filteredOutputFolderNames : string [ ] = [ ] ;
369
- for ( let i : number = 0 ; i < outputFolderNamesThatExist . length ; i ++ ) {
370
- if ( outputFolderNamesThatExist [ i ] ) {
371
- filteredOutputFolderNames . push ( this . _projectOutputFolderNames [ i ] ) ;
372
- }
373
- }
374
381
375
- let encounteredEnumerationIssue : boolean = false ;
376
- function symbolicLinkPathCallback ( entryPath : string ) : void {
377
- terminal . writeError ( `Unable to include "${ entryPath } " in build cache. It is a symbolic link.` ) ;
378
- encounteredEnumerationIssue = true ;
379
- }
382
+ let hasSymbolicLinks : boolean = false ;
380
383
381
- const outputFilePaths : string [ ] = [ ] ;
382
- for ( const filteredOutputFolderName of filteredOutputFolderNames ) {
383
- if ( encounteredEnumerationIssue ) {
384
- return undefined ;
384
+ // Adds child directories to the queue, files to the path list, and bails on symlinks
385
+ function processChildren ( relativePath : string , diskPath : string , children : FolderItem [ ] ) : void {
386
+ for ( const child of children ) {
387
+ const childRelativePath : string = `${ relativePath } /${ child . name } ` ;
388
+ if ( child . isSymbolicLink ( ) ) {
389
+ terminal . writeError (
390
+ `Unable to include "${ childRelativePath } " in build cache. It is a symbolic link.`
391
+ ) ;
392
+ hasSymbolicLinks = true ;
393
+ } else if ( child . isDirectory ( ) ) {
394
+ queue . push ( [ childRelativePath , `${ diskPath } /${ child . name } ` ] ) ;
395
+ } else {
396
+ outputFilePaths . push ( childRelativePath ) ;
397
+ }
385
398
}
399
+ }
386
400
387
- const outputFilePathsForFolder : AsyncIterableIterator < string > = this . _getPathsInFolder (
388
- terminal ,
389
- symbolicLinkPathCallback ,
390
- filteredOutputFolderName ,
391
- `${ projectFolderPath } /${ filteredOutputFolderName } `
392
- ) ;
401
+ // Handle declared output folders.
402
+ for ( const outputFolder of this . _projectOutputFolderNames ) {
403
+ const diskPath : string = `${ posixPrefix } /${ outputFolder } ` ;
404
+ try {
405
+ const children : FolderItem [ ] = await FileSystem . readFolderItemsAsync ( diskPath ) ;
406
+ processChildren ( outputFolder , diskPath , children ) ;
407
+ // The folder exists, record it
408
+ filteredOutputFolderNames . push ( outputFolder ) ;
409
+ } catch ( error ) {
410
+ if ( ! FileSystem . isNotExistError ( error as Error ) ) {
411
+ throw error ;
412
+ }
393
413
394
- for await ( const outputFilePath of outputFilePathsForFolder ) {
395
- outputFilePaths . push ( outputFilePath ) ;
414
+ // If the folder does not exist, ignore it.
396
415
}
397
416
}
398
417
399
- if ( encounteredEnumerationIssue ) {
418
+ // Walk the tree in parallel
419
+ await Async . forEachAsync (
420
+ queue ,
421
+ async ( [ relativePath , diskPath ] : [ string , string ] ) => {
422
+ const children : FolderItem [ ] = await FileSystem . readFolderItemsAsync ( diskPath ) ;
423
+ processChildren ( relativePath , diskPath , children ) ;
424
+ } ,
425
+ {
426
+ concurrency : 10
427
+ }
428
+ ) ;
429
+
430
+ if ( hasSymbolicLinks ) {
431
+ // Symbolic links do not round-trip safely.
400
432
return undefined ;
401
433
}
402
434
435
+ // Ensure stable output path order.
436
+ outputFilePaths . sort ( ) ;
437
+
403
438
return {
404
- filteredOutputFolderNames ,
405
- outputFilePaths
439
+ outputFilePaths ,
440
+ filteredOutputFolderNames
406
441
} ;
407
442
}
408
443
409
- private async * _getPathsInFolder (
410
- terminal : ITerminal ,
411
- symbolicLinkPathCallback : ( path : string ) => void ,
412
- posixPrefix : string ,
413
- folderPath : string
414
- ) : AsyncIterableIterator < string > {
415
- const folderEntries : FolderItem [ ] = await FileSystem . readFolderItemsAsync ( folderPath ) ;
416
- for ( const folderEntry of folderEntries ) {
417
- const entryPath : string = `${ posixPrefix } /${ folderEntry . name } ` ;
418
- if ( folderEntry . isSymbolicLink ( ) ) {
419
- symbolicLinkPathCallback ( entryPath ) ;
420
- } else if ( folderEntry . isDirectory ( ) ) {
421
- yield * this . _getPathsInFolder (
422
- terminal ,
423
- symbolicLinkPathCallback ,
424
- entryPath ,
425
- `${ folderPath } /${ folderEntry . name } `
426
- ) ;
427
- } else {
428
- yield entryPath ;
429
- }
430
- }
431
- }
432
-
433
444
private _getTarLogFilePath ( ) : string {
434
- return path . join ( this . _project . projectRushTempFolder , 'build-cache-tar. log' ) ;
445
+ return path . join ( this . _project . projectRushTempFolder , ` ${ this . _cacheId } . log` ) ;
435
446
}
436
447
437
448
private static async _getCacheId ( options : IProjectBuildCacheOptions ) : Promise < string | undefined > {
0 commit comments