diff --git a/apps/heft/src/operations/runners/TaskOperationRunner.ts b/apps/heft/src/operations/runners/TaskOperationRunner.ts index 66274a696a1..2ef05245440 100644 --- a/apps/heft/src/operations/runners/TaskOperationRunner.ts +++ b/apps/heft/src/operations/runners/TaskOperationRunner.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { createHash, type Hash } from 'node:crypto'; + import { type IOperationRunner, type IOperationRunnerContext, @@ -10,7 +12,12 @@ import { import { AlreadyReportedError, InternalError } from '@rushstack/node-core-library'; import type { HeftTask } from '../../pluginFramework/HeftTask'; -import { copyFilesAsync, normalizeCopyOperation } from '../../plugins/CopyFilesPlugin'; +import { + copyFilesAsync, + type ICopyOperation, + asAbsoluteCopyOperation, + asRelativeCopyOperation +} from '../../plugins/CopyFilesPlugin'; import { deleteFilesAsync } from '../../plugins/DeleteFilesPlugin'; import type { HeftTaskSession, @@ -51,6 +58,7 @@ export class TaskOperationRunner implements IOperationRunner { private readonly _options: ITaskOperationRunnerOptions; private _fileOperations: IHeftTaskFileOperations | undefined = undefined; + private _copyConfigHash: string | undefined; private _watchFileSystemAdapter: WatchFileSystemAdapter | undefined = undefined; public readonly silent: boolean = false; @@ -99,12 +107,31 @@ export class TaskOperationRunner implements IOperationRunner { deleteOperations: new Set() }); - // Do this here so that we only need to do it once for each run - for (const copyOperation of fileOperations.copyOperations) { - normalizeCopyOperation(rootFolderPath, copyOperation); + let copyConfigHash: string | undefined; + const { copyOperations } = fileOperations; + if (copyOperations.size > 0) { + // Do this here so that we only need to do it once for each Heft invocation + const hasher: Hash | undefined = createHash('sha256'); + const absolutePathCopyOperations: Set = new Set(); + for (const copyOperation of fileOperations.copyOperations) { + // The paths in the `fileOperations` object may be either absolute or relative + // For execution we need absolute paths. + const absoluteOperation: ICopyOperation = asAbsoluteCopyOperation(rootFolderPath, copyOperation); + absolutePathCopyOperations.add(absoluteOperation); + + // For portability of the hash we need relative paths. + const portableCopyOperation: ICopyOperation = asRelativeCopyOperation( + rootFolderPath, + absoluteOperation + ); + hasher.update(JSON.stringify(portableCopyOperation)); + } + fileOperations.copyOperations = absolutePathCopyOperations; + copyConfigHash = hasher.digest('base64'); } this._fileOperations = fileOperations; + this._copyConfigHash = copyConfigHash; } const shouldRunIncremental: boolean = isWatchMode && hooks.runIncremental.isUsed(); @@ -177,13 +204,15 @@ export class TaskOperationRunner implements IOperationRunner { if (this._fileOperations) { const { copyOperations, deleteOperations } = this._fileOperations; + const copyConfigHash: string | undefined = this._copyConfigHash; await Promise.all([ - copyOperations.size > 0 + copyConfigHash ? copyFilesAsync( copyOperations, logger.terminal, `${taskSession.tempFolderPath}/file-copy.json`, + copyConfigHash, isWatchMode ? getWatchFileSystemAdapter() : undefined ) : Promise.resolve(), diff --git a/apps/heft/src/pluginFramework/IncrementalBuildInfo.ts b/apps/heft/src/pluginFramework/IncrementalBuildInfo.ts index 93630a4a1d9..56eb74ed6b9 100644 --- a/apps/heft/src/pluginFramework/IncrementalBuildInfo.ts +++ b/apps/heft/src/pluginFramework/IncrementalBuildInfo.ts @@ -55,7 +55,7 @@ export interface ISerializedIncrementalBuildInfo { /** * Converts an absolute path to a path relative to a base path. */ -const makePathRelative: (absolutePath: string, basePath: string) => string = +export const makePathRelative: (absolutePath: string, basePath: string) => string = process.platform === 'win32' ? (absolutePath: string, basePath: string) => { // On Windows, need to normalize slashes diff --git a/apps/heft/src/plugins/CopyFilesPlugin.ts b/apps/heft/src/plugins/CopyFilesPlugin.ts index 6c2a30183de..2c9d47347a1 100644 --- a/apps/heft/src/plugins/CopyFilesPlugin.ts +++ b/apps/heft/src/plugins/CopyFilesPlugin.ts @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { createHash, type Hash } from 'crypto'; -import type * as fs from 'fs'; -import * as path from 'path'; +import { createHash } from 'node:crypto'; +import type * as fs from 'node:fs'; +import * as path from 'node:path'; import { AlreadyExistsBehavior, FileSystem, Async } from '@rushstack/node-core-library'; import type { ITerminal } from '@rushstack/terminal'; import { Constants } from '../utilities/Constants'; import { - normalizeFileSelectionSpecifier, + asAbsoluteFileSelectionSpecifier, getFileSelectionSpecifierPathsAsync, type IFileSelectionSpecifier } from './FileGlobSpecifier'; @@ -20,6 +20,7 @@ import type { IHeftTaskSession, IHeftTaskFileOperations } from '../pluginFramewo import type { WatchFileSystemAdapter } from '../utilities/WatchFileSystemAdapter'; import { type IIncrementalBuildInfo, + makePathRelative, tryReadBuildInfoAsync, writeBuildInfoAsync } from '../pluginFramework/IncrementalBuildInfo'; @@ -79,25 +80,40 @@ interface ICopyDescriptor { hardlink: boolean; } -export function normalizeCopyOperation(rootFolderPath: string, copyOperation: ICopyOperation): void { - normalizeFileSelectionSpecifier(rootFolderPath, copyOperation); - copyOperation.destinationFolders = copyOperation.destinationFolders.map((x) => - path.resolve(rootFolderPath, x) +export function asAbsoluteCopyOperation( + rootFolderPath: string, + copyOperation: ICopyOperation +): ICopyOperation { + const absoluteCopyOperation: ICopyOperation = asAbsoluteFileSelectionSpecifier( + rootFolderPath, + copyOperation ); + absoluteCopyOperation.destinationFolders = copyOperation.destinationFolders.map((folder) => + path.resolve(rootFolderPath, folder) + ); + return absoluteCopyOperation; +} + +export function asRelativeCopyOperation( + rootFolderPath: string, + copyOperation: ICopyOperation +): ICopyOperation { + return { + ...copyOperation, + destinationFolders: copyOperation.destinationFolders.map((folder) => + makePathRelative(folder, rootFolderPath) + ), + sourcePath: copyOperation.sourcePath && makePathRelative(copyOperation.sourcePath, rootFolderPath) + }; } export async function copyFilesAsync( copyOperations: Iterable, terminal: ITerminal, buildInfoPath: string, + configHash: string, watchFileSystemAdapter?: WatchFileSystemAdapter ): Promise { - const hasher: Hash = createHash('sha256'); - for (const copyOperation of copyOperations) { - hasher.update(JSON.stringify(copyOperation)); - } - const configHash: string = hasher.digest('base64'); - const copyDescriptorByDestination: Map = await _getCopyDescriptorsAsync( copyOperations, watchFileSystemAdapter diff --git a/apps/heft/src/plugins/DeleteFilesPlugin.ts b/apps/heft/src/plugins/DeleteFilesPlugin.ts index 36bb7f19ff3..e85047a6597 100644 --- a/apps/heft/src/plugins/DeleteFilesPlugin.ts +++ b/apps/heft/src/plugins/DeleteFilesPlugin.ts @@ -8,7 +8,7 @@ import type { ITerminal } from '@rushstack/terminal'; import { Constants } from '../utilities/Constants'; import { getFileSelectionSpecifierPathsAsync, - normalizeFileSelectionSpecifier, + asAbsoluteFileSelectionSpecifier, type IFileSelectionSpecifier } from './FileGlobSpecifier'; import type { HeftConfiguration } from '../configuration/HeftConfiguration'; @@ -43,11 +43,14 @@ async function _getPathsToDeleteAsync( await Async.forEachAsync( deleteOperations, async (deleteOperation: IDeleteOperation) => { - normalizeFileSelectionSpecifier(rootFolderPath, deleteOperation); + const absoluteSpecifier: IDeleteOperation = asAbsoluteFileSelectionSpecifier( + rootFolderPath, + deleteOperation + ); // Glob the files under the source path and add them to the set of files to delete const sourcePaths: Map = await getFileSelectionSpecifierPathsAsync({ - fileGlobSpecifier: deleteOperation, + fileGlobSpecifier: absoluteSpecifier, includeFolders: true }); for (const [sourcePath, dirent] of sourcePaths) { diff --git a/apps/heft/src/plugins/FileGlobSpecifier.ts b/apps/heft/src/plugins/FileGlobSpecifier.ts index d5e4a2d5c74..8f147cb148b 100644 --- a/apps/heft/src/plugins/FileGlobSpecifier.ts +++ b/apps/heft/src/plugins/FileGlobSpecifier.ts @@ -178,13 +178,17 @@ export async function getFileSelectionSpecifierPathsAsync( return results; } -export function normalizeFileSelectionSpecifier( +export function asAbsoluteFileSelectionSpecifier( rootPath: string, - fileGlobSpecifier: IFileSelectionSpecifier -): void { + fileGlobSpecifier: TSpecifier +): TSpecifier { const { sourcePath } = fileGlobSpecifier; - fileGlobSpecifier.sourcePath = sourcePath ? path.resolve(rootPath, sourcePath) : rootPath; - fileGlobSpecifier.includeGlobs = getIncludedGlobPatterns(fileGlobSpecifier); + return { + ...fileGlobSpecifier, + sourcePath: sourcePath ? path.resolve(rootPath, sourcePath) : rootPath, + includeGlobs: getIncludedGlobPatterns(fileGlobSpecifier), + fileExtensions: undefined + }; } function getIncludedGlobPatterns(fileGlobSpecifier: IFileSelectionSpecifier): string[] { diff --git a/common/changes/@rushstack/heft/copy-files-hash_2024-10-01-20-05.json b/common/changes/@rushstack/heft/copy-files-hash_2024-10-01-20-05.json new file mode 100644 index 00000000000..0aecf7c0563 --- /dev/null +++ b/common/changes/@rushstack/heft/copy-files-hash_2024-10-01-20-05.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft", + "comment": "Ensure `configHash` for file copy incremental cache file is portable.", + "type": "patch" + } + ], + "packageName": "@rushstack/heft" +} \ No newline at end of file