Skip to content

[heft] Fix portability of configHash for incremental file copy #4955

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions apps/heft/src/operations/runners/TaskOperationRunner.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<ICopyOperation> = 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();
Expand Down Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion apps/heft/src/pluginFramework/IncrementalBuildInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 30 additions & 14 deletions apps/heft/src/plugins/CopyFilesPlugin.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<ICopyOperation>,
terminal: ITerminal,
buildInfoPath: string,
configHash: string,
watchFileSystemAdapter?: WatchFileSystemAdapter
): Promise<void> {
const hasher: Hash = createHash('sha256');
for (const copyOperation of copyOperations) {
hasher.update(JSON.stringify(copyOperation));
}
const configHash: string = hasher.digest('base64');

const copyDescriptorByDestination: Map<string, ICopyDescriptor> = await _getCopyDescriptorsAsync(
copyOperations,
watchFileSystemAdapter
Expand Down
9 changes: 6 additions & 3 deletions apps/heft/src/plugins/DeleteFilesPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string, fs.Dirent> = await getFileSelectionSpecifierPathsAsync({
fileGlobSpecifier: deleteOperation,
fileGlobSpecifier: absoluteSpecifier,
includeFolders: true
});
for (const [sourcePath, dirent] of sourcePaths) {
Expand Down
14 changes: 9 additions & 5 deletions apps/heft/src/plugins/FileGlobSpecifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,17 @@ export async function getFileSelectionSpecifierPathsAsync(
return results;
}

export function normalizeFileSelectionSpecifier(
export function asAbsoluteFileSelectionSpecifier<TSpecifier extends IFileSelectionSpecifier>(
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[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/heft",
"comment": "Ensure `configHash` for file copy incremental cache file is portable.",
"type": "patch"
}
],
"packageName": "@rushstack/heft"
}
Loading