From 4eee6bcff38bbf86ad2623ecd3dcb69990324ae2 Mon Sep 17 00:00:00 2001 From: David Michon Date: Fri, 7 Feb 2025 00:56:59 +0000 Subject: [PATCH 1/4] [rush] Add `createEnvironmentForOperation` hook --- ...ustomize-environment_2025-02-07-00-14.json | 10 ++ common/reviews/api/rush-lib.api.md | 6 + .../cli/scriptActions/PhasedScriptAction.ts | 5 + .../src/logic/operations/IOperationRunner.ts | 7 ++ .../logic/operations/IPCOperationRunner.ts | 36 +++--- .../operations/IPCOperationRunnerPlugin.ts | 7 +- .../operations/OperationExecutionManager.ts | 8 +- .../operations/OperationExecutionRecord.ts | 6 + .../logic/operations/ShellOperationRunner.ts | 29 ++--- .../operations/ShellOperationRunnerPlugin.ts | 103 +++++++++--------- .../src/pluginFramework/PhasedCommandHooks.ts | 12 +- 11 files changed, 143 insertions(+), 86 deletions(-) create mode 100644 common/changes/@microsoft/rush/customize-environment_2025-02-07-00-14.json diff --git a/common/changes/@microsoft/rush/customize-environment_2025-02-07-00-14.json b/common/changes/@microsoft/rush/customize-environment_2025-02-07-00-14.json new file mode 100644 index 00000000000..4069a4900e1 --- /dev/null +++ b/common/changes/@microsoft/rush/customize-environment_2025-02-07-00-14.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add a new phased command hook `createEnvironmentForOperation` that can be used to customize the environment variables passed to individual operation subprocesses. This may be used to, for example, customize `NODE_OPTIONS` to pass `--diagnostic-dir` or other such parameters.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 5ead502d072..26883d6c46c 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -25,6 +25,7 @@ import { LookupByPath } from '@rushstack/lookup-by-path'; import { PackageNameParser } from '@rushstack/node-core-library'; import type { StdioSummarizer } from '@rushstack/terminal'; import { SyncHook } from 'tapable'; +import { SyncWaterfallHook } from 'tapable'; import { Terminal } from '@rushstack/terminal'; // @public @@ -644,6 +645,7 @@ export interface IOperationRunner { export interface IOperationRunnerContext { collatedWriter: CollatedWriter; debugMode: boolean; + environment: IEnvironment | undefined; error?: Error; // @internal _operationMetadataManager?: _OperationMetadataManager; @@ -1078,6 +1080,10 @@ export class PhasedCommandHooks { IExecuteOperationsContext ]>; readonly beforeLog: SyncHook; + readonly createEnvironmentForOperation: SyncWaterfallHook<[ + IEnvironment, + IOperationRunnerContext & IOperationExecutionResult + ]>; readonly createOperations: AsyncSeriesWaterfallHook<[Set, ICreateOperationsContext]>; readonly onOperationStatusChanged: SyncHook<[IOperationExecutionResult]>; readonly shutdownAsync: AsyncParallelHook; diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 0cb0b85ccff..88308fa98ef 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -507,6 +507,11 @@ export class PhasedScriptAction extends BaseScriptAction { afterExecuteOperationAsync: async (record: OperationExecutionRecord) => { await this.hooks.afterExecuteOperation.promise(record); }, + createEnvironmentForOperation: this.hooks.createEnvironmentForOperation.isUsed() + ? (record: OperationExecutionRecord) => { + return this.hooks.createEnvironmentForOperation.call({ ...process.env }, record); + } + : undefined, onOperationStatusChangedAsync: (record: OperationExecutionRecord) => { this.hooks.onOperationStatusChanged.call(record); } diff --git a/libraries/rush-lib/src/logic/operations/IOperationRunner.ts b/libraries/rush-lib/src/logic/operations/IOperationRunner.ts index 9a8ebdbb8c5..3321528fdf4 100644 --- a/libraries/rush-lib/src/logic/operations/IOperationRunner.ts +++ b/libraries/rush-lib/src/logic/operations/IOperationRunner.ts @@ -7,6 +7,7 @@ import type { CollatedWriter } from '@rushstack/stream-collator'; import type { OperationStatus } from './OperationStatus'; import type { OperationMetadataManager } from './OperationMetadataManager'; import type { IStopwatchResult } from '../../utilities/Stopwatch'; +import type { IEnvironment } from '../../utilities/Utilities'; /** * Information passed to the executing `IOperationRunner` @@ -44,6 +45,12 @@ export interface IOperationRunnerContext { */ status: OperationStatus; + /** + * The environment in which the operation is being executed. + * A return value of `undefined` indicates that it should inherit the environment from the parent process. + */ + environment: IEnvironment | undefined; + /** * Error which occurred while executing this operation, this is stored in case we need * it later (for example to re-print errors at end of execution). diff --git a/libraries/rush-lib/src/logic/operations/IPCOperationRunner.ts b/libraries/rush-lib/src/logic/operations/IPCOperationRunner.ts index cf18a24f44f..11b5dd1bcbe 100644 --- a/libraries/rush-lib/src/logic/operations/IPCOperationRunner.ts +++ b/libraries/rush-lib/src/logic/operations/IPCOperationRunner.ts @@ -15,7 +15,6 @@ import { TerminalProviderSeverity, type ITerminal, type ITerminalProvider } from import type { IPhase } from '../../api/CommandLineConfiguration'; import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; -import type { RushConfiguration } from '../../api/RushConfiguration'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { Utilities } from '../../utilities/Utilities'; import type { IOperationRunner, IOperationRunnerContext } from './IOperationRunner'; @@ -26,7 +25,8 @@ export interface IIPCOperationRunnerOptions { phase: IPhase; project: RushConfigurationProject; name: string; - shellCommand: string; + commandToRun: string; + commandForHash: string; persist: boolean; requestRun: (requestor?: string) => void; } @@ -53,9 +53,9 @@ export class IPCOperationRunner implements IOperationRunner { public readonly silent: boolean = false; public readonly warningsAreAllowed: boolean; - private readonly _rushConfiguration: RushConfiguration; - private readonly _shellCommand: string; - private readonly _workingDirectory: string; + private readonly _rushProject: RushConfigurationProject; + private readonly _commandToRun: string; + private readonly _commandForHash: string; private readonly _persist: boolean; private readonly _requestRun: (requestor?: string) => void; @@ -68,9 +68,10 @@ export class IPCOperationRunner implements IOperationRunner { EnvironmentConfiguration.allowWarningsInSuccessfulBuild || options.phase.allowWarningsOnSuccess || false; - this._rushConfiguration = options.project.rushConfiguration; - this._shellCommand = options.shellCommand; - this._workingDirectory = options.project.projectFolder; + this._rushProject = options.project; + this._commandToRun = options.commandToRun; + this._commandForHash = options.commandForHash; + this._persist = options.persist; this._requestRun = options.requestRun; } @@ -81,18 +82,23 @@ export class IPCOperationRunner implements IOperationRunner { let isConnected: boolean = false; if (!this._ipcProcess || typeof this._ipcProcess.exitCode === 'number') { // Run the operation - terminal.writeLine('Invoking: ' + this._shellCommand); + terminal.writeLine('Invoking: ' + this._commandToRun); + + const { rushConfiguration, projectFolder } = this._rushProject; + + const { environment: initialEnvironment } = context; - this._ipcProcess = Utilities.executeLifecycleCommandAsync(this._shellCommand, { - rushConfiguration: this._rushConfiguration, - workingDirectory: this._workingDirectory, - initCwd: this._rushConfiguration.commonTempFolder, + this._ipcProcess = Utilities.executeLifecycleCommandAsync(this._commandToRun, { + rushConfiguration, + workingDirectory: projectFolder, + initCwd: rushConfiguration.commonTempFolder, handleOutput: true, environmentPathOptions: { includeProjectBin: true }, ipc: true, - connectSubprocessTerminator: true + connectSubprocessTerminator: true, + initialEnvironment }); let resolveReadyPromise!: () => void; @@ -193,7 +199,7 @@ export class IPCOperationRunner implements IOperationRunner { } public getConfigHash(): string { - return this._shellCommand; + return this._commandForHash; } public async shutdownAsync(): Promise { diff --git a/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts b/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts index bcbc6f9a281..f8fe9bc0f40 100644 --- a/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts @@ -69,6 +69,10 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin { continue; } + // This is the command that will be used to identify the cache entry for this operation, to allow + // for this operation (or downstream operations) to be restored from the build cache. + const commandForHash: string | undefined = phase.shellCommand ?? scripts?.[phaseName]; + const customParameterValues: ReadonlyArray = getCustomParameterValuesForPhase(phase); const commandToRun: string = formatCommand(rawScript, customParameterValues); @@ -79,7 +83,8 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin { phase, project, name: operationName, - shellCommand: commandToRun, + commandToRun, + commandForHash, persist: true, requestRun: (requestor?: string) => { const operationState: IOperationExecutionResult | undefined = diff --git a/libraries/rush-lib/src/logic/operations/OperationExecutionManager.ts b/libraries/rush-lib/src/logic/operations/OperationExecutionManager.ts index bd54fc9ff81..a454c118fdc 100644 --- a/libraries/rush-lib/src/logic/operations/OperationExecutionManager.ts +++ b/libraries/rush-lib/src/logic/operations/OperationExecutionManager.ts @@ -17,6 +17,7 @@ import type { Operation } from './Operation'; import { OperationStatus } from './OperationStatus'; import { type IOperationExecutionRecordContext, OperationExecutionRecord } from './OperationExecutionRecord'; import type { IExecutionResult } from './IOperationExecutionResult'; +import type { IEnvironment } from '../../utilities/Utilities'; export interface IOperationExecutionManagerOptions { quietMode: boolean; @@ -27,6 +28,7 @@ export interface IOperationExecutionManagerOptions { beforeExecuteOperationAsync?: (operation: OperationExecutionRecord) => Promise; afterExecuteOperationAsync?: (operation: OperationExecutionRecord) => Promise; + createEnvironmentForOperation?: (operation: OperationExecutionRecord) => IEnvironment; onOperationStatusChangedAsync?: (record: OperationExecutionRecord) => void; beforeExecuteOperationsAsync?: (records: Map) => Promise; } @@ -70,6 +72,7 @@ export class OperationExecutionManager { private readonly _beforeExecuteOperations?: ( records: Map ) => Promise; + private readonly _createEnvironmentForOperation?: (operation: OperationExecutionRecord) => IEnvironment; // Variables for current status private _hasAnyFailures: boolean; @@ -86,7 +89,8 @@ export class OperationExecutionManager { beforeExecuteOperationAsync: beforeExecuteOperation, afterExecuteOperationAsync: afterExecuteOperation, onOperationStatusChangedAsync: onOperationStatusChanged, - beforeExecuteOperationsAsync: beforeExecuteOperations + beforeExecuteOperationsAsync: beforeExecuteOperations, + createEnvironmentForOperation } = options; this._completedOperations = 0; this._quietMode = quietMode; @@ -98,6 +102,7 @@ export class OperationExecutionManager { this._beforeExecuteOperation = beforeExecuteOperation; this._afterExecuteOperation = afterExecuteOperation; this._beforeExecuteOperations = beforeExecuteOperations; + this._createEnvironmentForOperation = createEnvironmentForOperation; this._onOperationStatusChanged = (record: OperationExecutionRecord) => { if (record.status === OperationStatus.Ready) { this._executionQueue.assignOperations(); @@ -125,6 +130,7 @@ export class OperationExecutionManager { const executionRecordContext: IOperationExecutionRecordContext = { streamCollator: this._streamCollator, onOperationStatusChanged: this._onOperationStatusChanged, + createEnvironment: this._createEnvironmentForOperation, debugMode, quietMode }; diff --git a/libraries/rush-lib/src/logic/operations/OperationExecutionRecord.ts b/libraries/rush-lib/src/logic/operations/OperationExecutionRecord.ts index 54ca0331531..24f233a12ec 100644 --- a/libraries/rush-lib/src/logic/operations/OperationExecutionRecord.ts +++ b/libraries/rush-lib/src/logic/operations/OperationExecutionRecord.ts @@ -33,10 +33,12 @@ import type { IOperationExecutionResult } from './IOperationExecutionResult'; import type { IInputsSnapshot } from '../incremental/InputsSnapshot'; import { RushConstants } from '../RushConstants'; import type { BuildCacheConfiguration } from '../../api/BuildCacheConfiguration'; +import type { IEnvironment } from '../../utilities/Utilities'; export interface IOperationExecutionRecordContext { streamCollator: StreamCollator; onOperationStatusChanged?: (record: OperationExecutionRecord) => void; + createEnvironment?: (record: OperationExecutionRecord) => IEnvironment; debugMode: boolean; quietMode: boolean; @@ -182,6 +184,10 @@ export class OperationExecutionRecord implements IOperationRunnerContext, IOpera return this._operationMetadataManager?.stateFile.state?.cobuildRunnerId; } + public get environment(): IEnvironment | undefined { + return this._context.createEnvironment?.(this); + } + public get metadataFolderPath(): string | undefined { return this._operationMetadataManager?.metadataFolderPath; } diff --git a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts index e2576b17def..b24a622f508 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts @@ -8,21 +8,18 @@ import { type ITerminal, type ITerminalProvider, TerminalProviderSeverity } from import type { IPhase } from '../../api/CommandLineConfiguration'; import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; -import type { RushConfiguration } from '../../api/RushConfiguration'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; -import { type IEnvironment, Utilities } from '../../utilities/Utilities'; +import { Utilities } from '../../utilities/Utilities'; import type { IOperationRunner, IOperationRunnerContext } from './IOperationRunner'; import { OperationError } from './OperationError'; import { OperationStatus } from './OperationStatus'; -export interface IOperationRunnerOptions { +export interface IShellOperationRunnerOptions { + phase: IPhase; rushProject: RushConfigurationProject; - rushConfiguration: RushConfiguration; + displayName: string; commandToRun: string; commandForHash: string; - displayName: string; - phase: IPhase; - environment?: IEnvironment; } /** @@ -42,21 +39,16 @@ export class ShellOperationRunner implements IOperationRunner { private readonly _commandForHash: string; private readonly _rushProject: RushConfigurationProject; - private readonly _rushConfiguration: RushConfiguration; - private readonly _environment?: IEnvironment; - - public constructor(options: IOperationRunnerOptions) { + public constructor(options: IShellOperationRunnerOptions) { const { phase } = options; this.name = options.displayName; this.warningsAreAllowed = EnvironmentConfiguration.allowWarningsInSuccessfulBuild || phase.allowWarningsOnSuccess || false; this._rushProject = options.rushProject; - this._rushConfiguration = options.rushConfiguration; this._commandToRun = options.commandToRun; this._commandForHash = options.commandForHash; - this._environment = options.environment; } public async executeAsync(context: IOperationRunnerContext): Promise { @@ -75,22 +67,25 @@ export class ShellOperationRunner implements IOperationRunner { return await context.runWithTerminalAsync( async (terminal: ITerminal, terminalProvider: ITerminalProvider) => { let hasWarningOrError: boolean = false; - const projectFolder: string = this._rushProject.projectFolder; // Run the operation terminal.writeLine('Invoking: ' + this._commandToRun); + const { rushConfiguration, projectFolder } = this._rushProject; + + const { environment: initialEnvironment } = context; + const subProcess: child_process.ChildProcess = Utilities.executeLifecycleCommandAsync( this._commandToRun, { - rushConfiguration: this._rushConfiguration, + rushConfiguration: rushConfiguration, workingDirectory: projectFolder, - initCwd: this._rushConfiguration.commonTempFolder, + initCwd: rushConfiguration.commonTempFolder, handleOutput: true, environmentPathOptions: { includeProjectBin: true }, - initialEnvironment: this._environment + initialEnvironment } ); diff --git a/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts b/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts index 3baad699275..10848ecbd62 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts @@ -15,6 +15,7 @@ import type { import type { Operation } from './Operation'; import type { RushConfiguration } from '../../api/RushConfiguration'; import type { IOperationRunner } from './IOperationRunner'; +import type { IEnvironment } from '../../utilities/Utilities'; export const PLUGIN_NAME: 'ShellOperationRunnerPlugin' = 'ShellOperationRunnerPlugin'; @@ -23,54 +24,55 @@ export const PLUGIN_NAME: 'ShellOperationRunnerPlugin' = 'ShellOperationRunnerPl */ export class ShellOperationRunnerPlugin implements IPhasedCommandPlugin { public apply(hooks: PhasedCommandHooks): void { - hooks.createOperations.tap(PLUGIN_NAME, createShellOperations); - } -} - -function createShellOperations( - operations: Set, - context: ICreateOperationsContext -): Set { - const { rushConfiguration, isInitial } = context; - - const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray = - getCustomParameterValuesByPhase(); - for (const operation of operations) { - const { associatedPhase: phase, associatedProject: project } = operation; - - if (phase && project && !operation.runner) { - // This is a shell command. In the future, may consider having a property on the initial operation - // to specify a runner type requested in rush-project.json - const customParameterValues: ReadonlyArray = getCustomParameterValuesForPhase(phase); - - const displayName: string = getDisplayName(phase, project); - const { name: phaseName, shellCommand } = phase; - - const { scripts } = project.packageJson; - - // This is the command that will be used to identify the cache entry for this operation - const commandForHash: string | undefined = shellCommand ?? scripts?.[phaseName]; - - // For execution of non-initial runs, prefer the `:incremental` script if it exists. - // However, the `shellCommand` value still takes precedence per the spec for that feature. - const commandToRun: string | undefined = - shellCommand ?? - (!isInitial ? scripts?.[`${phaseName}:incremental`] : undefined) ?? - scripts?.[phaseName]; - - operation.runner = initializeShellOperationRunner({ - phase, - project, - displayName, - commandForHash, - commandToRun, - customParameterValues, - rushConfiguration - }); - } + hooks.createOperations.tap( + PLUGIN_NAME, + function createShellOperations( + operations: Set, + context: ICreateOperationsContext + ): Set { + const { rushConfiguration, isInitial } = context; + + const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray = + getCustomParameterValuesByPhase(); + for (const operation of operations) { + const { associatedPhase: phase, associatedProject: project } = operation; + + if (phase && project && !operation.runner) { + // This is a shell command. In the future, may consider having a property on the initial operation + // to specify a runner type requested in rush-project.json + const customParameterValues: ReadonlyArray = getCustomParameterValuesForPhase(phase); + + const displayName: string = getDisplayName(phase, project); + const { name: phaseName, shellCommand } = phase; + + const { scripts } = project.packageJson; + + // This is the command that will be used to identify the cache entry for this operation + const commandForHash: string | undefined = shellCommand ?? scripts?.[phaseName]; + + // For execution of non-initial runs, prefer the `:incremental` script if it exists. + // However, the `shellCommand` value still takes precedence per the spec for that feature. + const commandToRun: string | undefined = + shellCommand ?? + (!isInitial ? scripts?.[`${phaseName}:incremental`] : undefined) ?? + scripts?.[phaseName]; + + operation.runner = initializeShellOperationRunner({ + phase, + project, + displayName, + commandForHash, + commandToRun, + customParameterValues, + rushConfiguration + }); + } + } + + return operations; + } + ); } - - return operations; } export function initializeShellOperationRunner(options: { @@ -91,11 +93,11 @@ export function initializeShellOperationRunner(options: { } if (rawCommandToRun) { - const { rushConfiguration, commandForHash: rawCommandForHash } = options; + const { commandForHash: rawCommandForHash, customParameterValues } = options; - const commandToRun: string = formatCommand(rawCommandToRun, options.customParameterValues); + const commandToRun: string = formatCommand(rawCommandToRun, customParameterValues); const commandForHash: string = rawCommandForHash - ? formatCommand(rawCommandForHash, options.customParameterValues) + ? formatCommand(rawCommandForHash, customParameterValues) : commandToRun; return new ShellOperationRunner({ @@ -103,7 +105,6 @@ export function initializeShellOperationRunner(options: { commandForHash, displayName, phase, - rushConfiguration, rushProject: project }); } else { diff --git a/libraries/rush-lib/src/pluginFramework/PhasedCommandHooks.ts b/libraries/rush-lib/src/pluginFramework/PhasedCommandHooks.ts index c0e9e7f764c..d0a9f4891bd 100644 --- a/libraries/rush-lib/src/pluginFramework/PhasedCommandHooks.ts +++ b/libraries/rush-lib/src/pluginFramework/PhasedCommandHooks.ts @@ -6,7 +6,8 @@ import { AsyncSeriesBailHook, AsyncSeriesHook, AsyncSeriesWaterfallHook, - SyncHook + SyncHook, + SyncWaterfallHook } from 'tapable'; import type { CommandLineParameter } from '@rushstack/ts-command-line'; @@ -25,6 +26,7 @@ import type { IOperationRunnerContext } from '../logic/operations/IOperationRunn import type { ITelemetryData } from '../logic/Telemetry'; import type { OperationStatus } from '../logic/operations/OperationStatus'; import type { IInputsSnapshot } from '../logic/incremental/InputsSnapshot'; +import type { IEnvironment } from '../utilities/Utilities'; /** * A plugin that interacts with a phased commands. @@ -156,6 +158,14 @@ export class PhasedCommandHooks { OperationStatus | undefined > = new AsyncSeriesBailHook(['runnerContext'], 'beforeExecuteOperation'); + /** + * Hook invoked to define environment variables for an operation. + * May be invoked by the runner to get the environment for the operation. + */ + public readonly createEnvironmentForOperation: SyncWaterfallHook< + [IEnvironment, IOperationRunnerContext & IOperationExecutionResult] + > = new SyncWaterfallHook(['environment', 'runnerContext'], 'createEnvironmentForOperation'); + /** * Hook invoked after executing a operation. */ From 2031c440a977b29e6634977ce428c34106fc9d70 Mon Sep 17 00:00:00 2001 From: David Michon Date: Fri, 7 Feb 2025 01:11:34 +0000 Subject: [PATCH 2/4] [rush] Add --node-diagnostic-dir parameter --- .../cpu-profiler-plugin_2025-02-07-01-10.json | 10 +++ .../RushCommandLine.test.ts.snap | 24 ++++++ .../cli/scriptActions/PhasedScriptAction.ts | 17 +++++ .../CommandLineHelp.test.ts.snap | 19 ++++- .../operations/NodeDiagnosticDirPlugin.ts | 75 +++++++++++++++++++ 5 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 common/changes/@microsoft/rush/cpu-profiler-plugin_2025-02-07-01-10.json create mode 100644 libraries/rush-lib/src/logic/operations/NodeDiagnosticDirPlugin.ts diff --git a/common/changes/@microsoft/rush/cpu-profiler-plugin_2025-02-07-01-10.json b/common/changes/@microsoft/rush/cpu-profiler-plugin_2025-02-07-01-10.json new file mode 100644 index 00000000000..6ce113c67b6 --- /dev/null +++ b/common/changes/@microsoft/rush/cpu-profiler-plugin_2025-02-07-01-10.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add a new command line parameter `--node-diagnostic-dir=DIR` to phased commands that, when specified, tells all child build processes to write NodeJS diagnostics into `${DIR}/${packageName}/${phaseIdentifier}`. This is useful if `--cpu-prof` or `--heap-prof` are enabled, to avoid polluting workspace folders.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap b/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap index f36ce6efc6f..b444c11fcd4 100644 --- a/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap +++ b/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap @@ -1244,6 +1244,14 @@ Object { "required": false, "shortName": undefined, }, + Object { + "description": "Specifies the directory where Node.js diagnostic reports will be written. This directory will contain a subdirectory for each project and phase.", + "environmentVariable": undefined, + "kind": "String", + "longName": "--node-diagnostic-dir", + "required": false, + "shortName": undefined, + }, Object { "description": "Selects a single instead of the default locale (en-us) for non-ship builds or all locales for ship builds.", "environmentVariable": undefined, @@ -1382,6 +1390,14 @@ Object { "required": false, "shortName": undefined, }, + Object { + "description": "Specifies the directory where Node.js diagnostic reports will be written. This directory will contain a subdirectory for each project and phase.", + "environmentVariable": undefined, + "kind": "String", + "longName": "--node-diagnostic-dir", + "required": false, + "shortName": undefined, + }, Object { "description": "Perform a production build, including minification and localization steps", "environmentVariable": undefined, @@ -1507,6 +1523,14 @@ Object { "required": false, "shortName": undefined, }, + Object { + "description": "Specifies the directory where Node.js diagnostic reports will be written. This directory will contain a subdirectory for each project and phase.", + "environmentVariable": undefined, + "kind": "String", + "longName": "--node-diagnostic-dir", + "required": false, + "shortName": undefined, + }, Object { "description": "Perform a production build, including minification and localization steps", "environmentVariable": undefined, diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 88308fa98ef..cbfdee147c0 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -57,6 +57,7 @@ import { FlagFile } from '../../api/FlagFile'; import { WeightedOperationPlugin } from '../../logic/operations/WeightedOperationPlugin'; import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; import { Selection } from '../../logic/Selection'; +import { NodeDiagnosticDirPlugin } from '../../logic/operations/NodeDiagnosticDirPlugin'; /** * Constructor parameters for PhasedScriptAction. @@ -153,6 +154,7 @@ export class PhasedScriptAction extends BaseScriptAction { private readonly _installParameter: CommandLineFlagParameter | undefined; private readonly _variantParameter: CommandLineStringParameter | undefined; private readonly _noIPCParameter: CommandLineFlagParameter | undefined; + private readonly _nodeDiagnosticDirParameter: CommandLineStringParameter; public constructor(options: IPhasedScriptActionOptions) { super(options); @@ -284,6 +286,14 @@ export class PhasedScriptAction extends BaseScriptAction { }); } + this._nodeDiagnosticDirParameter = this.defineStringParameter({ + parameterLongName: '--node-diagnostic-dir', + argumentName: 'DIRECTORY', + description: + 'Specifies the directory where Node.js diagnostic reports will be written. ' + + 'This directory will contain a subdirectory for each project and phase.' + }); + this.defineScriptParameters(); for (const [{ associatedPhases }, tsCommandLineParameter] of this.customParameters) { @@ -366,6 +376,13 @@ export class PhasedScriptAction extends BaseScriptAction { new ConsoleTimelinePlugin(terminal).apply(this.hooks); } + const diagnosticDir: string | undefined = this._nodeDiagnosticDirParameter.value; + if (diagnosticDir) { + new NodeDiagnosticDirPlugin({ + diagnosticDir + }).apply(this.hooks); + } + // Enable the standard summary new OperationResultSummarizerPlugin(terminal).apply(this.hooks); diff --git a/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index 007ff234fc5..12a9a43cd74 100644 --- a/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -155,7 +155,7 @@ exports[`CommandLineHelp prints the help for each action: build 1`] = ` [-i PROJECT] [-I PROJECT] [--to-version-policy VERSION_POLICY_NAME] [--from-version-policy VERSION_POLICY_NAME] [-v] [-c] - [--ignore-hooks] [-s] [-m] + [--ignore-hooks] [--node-diagnostic-dir DIRECTORY] [-s] [-m] This command is similar to \\"rush rebuild\\", except that \\"rush build\\" performs @@ -281,6 +281,10 @@ Optional arguments: --ignore-hooks Skips execution of the \\"eventHooks\\" scripts defined in rush.json. Make sure you know what you are skipping. + --node-diagnostic-dir DIRECTORY + Specifies the directory where Node.js diagnostic + reports will be written. This directory will contain + a subdirectory for each project and phase. -s, --ship Perform a production build, including minification and localization steps -m, --minimal Perform a fast build, which disables certain tasks @@ -432,7 +436,7 @@ exports[`CommandLineHelp prints the help for each action: import-strings 1`] = ` [-i PROJECT] [-I PROJECT] [--to-version-policy VERSION_POLICY_NAME] [--from-version-policy VERSION_POLICY_NAME] [-v] - [--ignore-hooks] + [--ignore-hooks] [--node-diagnostic-dir DIRECTORY] [--locale {en-us,fr-fr,es-es,zh-cn}] @@ -541,6 +545,10 @@ Optional arguments: --ignore-hooks Skips execution of the \\"eventHooks\\" scripts defined in rush.json. Make sure you know what you are skipping. + --node-diagnostic-dir DIRECTORY + Specifies the directory where Node.js diagnostic + reports will be written. This directory will contain + a subdirectory for each project and phase. --locale {en-us,fr-fr,es-es,zh-cn} Selects a single instead of the default locale (en-us) for non-ship builds or all locales for ship @@ -1030,7 +1038,8 @@ exports[`CommandLineHelp prints the help for each action: rebuild 1`] = ` [-i PROJECT] [-I PROJECT] [--to-version-policy VERSION_POLICY_NAME] [--from-version-policy VERSION_POLICY_NAME] [-v] - [--ignore-hooks] [-s] [-m] + [--ignore-hooks] [--node-diagnostic-dir DIRECTORY] [-s] + [-m] This command assumes that the package.json file for each project contains a @@ -1144,6 +1153,10 @@ Optional arguments: --ignore-hooks Skips execution of the \\"eventHooks\\" scripts defined in rush.json. Make sure you know what you are skipping. + --node-diagnostic-dir DIRECTORY + Specifies the directory where Node.js diagnostic + reports will be written. This directory will contain + a subdirectory for each project and phase. -s, --ship Perform a production build, including minification and localization steps -m, --minimal Perform a fast build, which disables certain tasks diff --git a/libraries/rush-lib/src/logic/operations/NodeDiagnosticDirPlugin.ts b/libraries/rush-lib/src/logic/operations/NodeDiagnosticDirPlugin.ts new file mode 100644 index 00000000000..1bb3d16eb0b --- /dev/null +++ b/libraries/rush-lib/src/logic/operations/NodeDiagnosticDirPlugin.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import path from 'path'; + +import { FileSystem } from '@rushstack/node-core-library'; + +import type { IPhasedCommandPlugin, PhasedCommandHooks } from '../../pluginFramework/PhasedCommandHooks'; +import type { IEnvironment } from '../../utilities/Utilities'; +import type { Operation } from './Operation'; +import type { IOperationRunnerContext } from './IOperationRunner'; +import type { IOperationExecutionResult } from './IOperationExecutionResult'; + +const PLUGIN_NAME: 'NodeDiagnosticDirPlugin' = 'NodeDiagnosticDirPlugin'; + +export interface INodeDiagnosticDirPluginOptions { + diagnosticDir: string; +} + +/** + * Phased command plugin that configures the NodeJS --diagnostic-dir option to contain the project and phase name. + */ +export class NodeDiagnosticDirPlugin implements IPhasedCommandPlugin { + private readonly _diagnosticsDir: string; + + public constructor(options: INodeDiagnosticDirPluginOptions) { + this._diagnosticsDir = options.diagnosticDir; + } + + public apply(hooks: PhasedCommandHooks): void { + const getDiagnosticDir = (operation: Operation): string | undefined => { + const { associatedProject } = operation; + + if (!associatedProject) { + return; + } + + const diagnosticDir: string = path.resolve( + this._diagnosticsDir, + associatedProject.packageName, + operation.logFilenameIdentifier + ); + + return diagnosticDir; + }; + + hooks.beforeExecuteOperation.tap( + PLUGIN_NAME, + (operation: IOperationRunnerContext & IOperationExecutionResult): undefined => { + const diagnosticDir: string | undefined = getDiagnosticDir(operation.operation); + if (!diagnosticDir) { + return; + } + + // Not all versions of NodeJS create the directory, so ensure it exists: + FileSystem.ensureFolder(diagnosticDir); + } + ); + + hooks.createEnvironmentForOperation.tap(PLUGIN_NAME, (env: IEnvironment, operation: Operation) => { + const diagnosticDir: string | undefined = getDiagnosticDir(operation); + if (!diagnosticDir) { + return env; + } + + const { NODE_OPTIONS } = env; + + const diagnosticDirEnv: string = `--diagnostic-dir="${diagnosticDir}"`; + + env.NODE_OPTIONS = NODE_OPTIONS ? `${NODE_OPTIONS} ${diagnosticDirEnv}` : diagnosticDirEnv; + + return env; + }); + } +} From 2d5024d57edd3af3a68a8579feffac59589288a3 Mon Sep 17 00:00:00 2001 From: David Michon Date: Fri, 7 Feb 2025 02:17:34 +0000 Subject: [PATCH 3/4] Address PR feedback --- .../operations/NodeDiagnosticDirPlugin.ts | 28 +++++++------------ .../logic/operations/ShellOperationRunner.ts | 2 +- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/libraries/rush-lib/src/logic/operations/NodeDiagnosticDirPlugin.ts b/libraries/rush-lib/src/logic/operations/NodeDiagnosticDirPlugin.ts index 1bb3d16eb0b..98d97e07ffb 100644 --- a/libraries/rush-lib/src/logic/operations/NodeDiagnosticDirPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/NodeDiagnosticDirPlugin.ts @@ -8,7 +8,6 @@ import { FileSystem } from '@rushstack/node-core-library'; import type { IPhasedCommandPlugin, PhasedCommandHooks } from '../../pluginFramework/PhasedCommandHooks'; import type { IEnvironment } from '../../utilities/Utilities'; import type { Operation } from './Operation'; -import type { IOperationRunnerContext } from './IOperationRunner'; import type { IOperationExecutionResult } from './IOperationExecutionResult'; const PLUGIN_NAME: 'NodeDiagnosticDirPlugin' = 'NodeDiagnosticDirPlugin'; @@ -44,32 +43,25 @@ export class NodeDiagnosticDirPlugin implements IPhasedCommandPlugin { return diagnosticDir; }; - hooks.beforeExecuteOperation.tap( + hooks.createEnvironmentForOperation.tap( PLUGIN_NAME, - (operation: IOperationRunnerContext & IOperationExecutionResult): undefined => { - const diagnosticDir: string | undefined = getDiagnosticDir(operation.operation); + (env: IEnvironment, record: IOperationExecutionResult) => { + const diagnosticDir: string | undefined = getDiagnosticDir(record.operation); if (!diagnosticDir) { - return; + return env; } // Not all versions of NodeJS create the directory, so ensure it exists: FileSystem.ensureFolder(diagnosticDir); - } - ); - - hooks.createEnvironmentForOperation.tap(PLUGIN_NAME, (env: IEnvironment, operation: Operation) => { - const diagnosticDir: string | undefined = getDiagnosticDir(operation); - if (!diagnosticDir) { - return env; - } - const { NODE_OPTIONS } = env; + const { NODE_OPTIONS } = env; - const diagnosticDirEnv: string = `--diagnostic-dir="${diagnosticDir}"`; + const diagnosticDirEnv: string = `--diagnostic-dir="${diagnosticDir}"`; - env.NODE_OPTIONS = NODE_OPTIONS ? `${NODE_OPTIONS} ${diagnosticDirEnv}` : diagnosticDirEnv; + env.NODE_OPTIONS = NODE_OPTIONS ? `${NODE_OPTIONS} ${diagnosticDirEnv}` : diagnosticDirEnv; - return env; - }); + return env; + } + ); } } diff --git a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts index b24a622f508..6f7be6867c7 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts @@ -69,7 +69,7 @@ export class ShellOperationRunner implements IOperationRunner { let hasWarningOrError: boolean = false; // Run the operation - terminal.writeLine('Invoking: ' + this._commandToRun); + terminal.writeLine(`Invoking: ${this._commandToRun}`); const { rushConfiguration, projectFolder } = this._rushProject; From 83230b2ab71cb18364612891645dfb1752c1a83c Mon Sep 17 00:00:00 2001 From: David Michon Date: Fri, 7 Feb 2025 03:05:40 +0000 Subject: [PATCH 4/4] fixup --- .../rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts b/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts index 10848ecbd62..6d26e98acca 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts @@ -15,7 +15,6 @@ import type { import type { Operation } from './Operation'; import type { RushConfiguration } from '../../api/RushConfiguration'; import type { IOperationRunner } from './IOperationRunner'; -import type { IEnvironment } from '../../utilities/Utilities'; export const PLUGIN_NAME: 'ShellOperationRunnerPlugin' = 'ShellOperationRunnerPlugin';