in apps/rush-lib/src/logic/taskExecution/ProjectTaskRunner.ts [132:398]
private async _executeTaskAsync(context: ITaskRunnerContext): Promise<TaskStatus> {
// TERMINAL PIPELINE:
//
// +--> quietModeTransform? --> collatedWriter
// |
// normalizeNewlineTransform --1--> stderrLineTransform --2--> removeColorsTransform --> projectLogWritable
// |
// +--> stdioSummarizer
const projectLogWritable: ProjectLogWritable = new ProjectLogWritable(
this._rushProject,
context.collatedWriter.terminal,
this._logFilenameIdentifier
);
try {
const removeColorsTransform: TextRewriterTransform = new TextRewriterTransform({
destination: projectLogWritable,
removeColors: true,
normalizeNewlines: NewlineKind.OsDefault
});
const splitterTransform2: SplitterTransform = new SplitterTransform({
destinations: [removeColorsTransform, context.stdioSummarizer]
});
const stderrLineTransform: StderrLineTransform = new StderrLineTransform({
destination: splitterTransform2,
newlineKind: NewlineKind.Lf // for StdioSummarizer
});
const discardTransform: DiscardStdoutTransform = new DiscardStdoutTransform({
destination: context.collatedWriter
});
const splitterTransform1: SplitterTransform = new SplitterTransform({
destinations: [context.quietMode ? discardTransform : context.collatedWriter, stderrLineTransform]
});
const normalizeNewlineTransform: TextRewriterTransform = new TextRewriterTransform({
destination: splitterTransform1,
normalizeNewlines: NewlineKind.Lf,
ensureNewlineAtEnd: true
});
const collatedTerminal: CollatedTerminal = new CollatedTerminal(normalizeNewlineTransform);
const terminalProvider: CollatedTerminalProvider = new CollatedTerminalProvider(collatedTerminal, {
debugEnabled: context.debugMode
});
const terminal: Terminal = new Terminal(terminalProvider);
let hasWarningOrError: boolean = false;
const projectFolder: string = this._rushProject.projectFolder;
let lastProjectDeps: IProjectDeps | undefined = undefined;
const currentDepsPath: string = path.join(
this._rushProject.projectRushTempFolder,
this._packageDepsFilename
);
if (FileSystem.exists(currentDepsPath)) {
try {
lastProjectDeps = JsonFile.load(currentDepsPath);
} catch (e) {
// Warn and ignore - treat failing to load the file as the project being not built.
terminal.writeWarningLine(
`Warning: error parsing ${this._packageDepsFilename}: ${e}. Ignoring and ` +
`treating the command "${this._commandToRun}" as not run.`
);
}
}
let projectDeps: IProjectDeps | undefined;
let trackedFiles: string[] | undefined;
try {
const fileHashes: Map<string, string> | undefined =
await this._projectChangeAnalyzer._tryGetProjectDependenciesAsync(this._rushProject, terminal);
if (fileHashes) {
const files: { [filePath: string]: string } = {};
trackedFiles = [];
for (const [filePath, fileHash] of fileHashes) {
files[filePath] = fileHash;
trackedFiles.push(filePath);
}
projectDeps = {
files,
arguments: this._commandToRun
};
} else if (this.isSkipAllowed) {
// To test this code path:
// Remove the `.git` folder then run "rush build --verbose"
terminal.writeLine({
text: PrintUtilities.wrapWords(
'This workspace does not appear to be tracked by Git. ' +
'Rush will proceed without incremental execution, caching, and change detection.'
),
foregroundColor: ColorValue.Cyan
});
}
} catch (error) {
// To test this code path:
// Delete a project's ".rush/temp/shrinkwrap-deps.json" then run "rush build --verbose"
terminal.writeLine('Unable to calculate incremental state: ' + (error as Error).toString());
terminal.writeLine({
text: 'Rush will proceed without incremental execution, caching, and change detection.',
foregroundColor: ColorValue.Cyan
});
}
// If possible, we want to skip this task -- either by restoring it from the
// cache, if caching is enabled, or determining that the project
// is unchanged (using the older incremental execution logic). These two approaches,
// "caching" and "skipping", are incompatible, so only one applies.
//
// Note that "caching" and "skipping" take two different approaches
// to tracking dependents:
//
// - For caching, "isCacheReadAllowed" is set if a project supports
// incremental builds, and determining whether this project or a dependent
// has changed happens inside the hashing logic.
//
// - For skipping, "isSkipAllowed" is set to true initially, and during
// the process of running dependents, it will be changed by TaskExecutionManager to
// false if a dependency wasn't able to be skipped.
//
let buildCacheReadAttempted: boolean = false;
if (this._isCacheReadAllowed) {
const projectBuildCache: ProjectBuildCache | undefined = await this._tryGetProjectBuildCacheAsync(
terminal,
trackedFiles,
context.repoCommandLineConfiguration
);
buildCacheReadAttempted = !!projectBuildCache;
const restoreFromCacheSuccess: boolean | undefined =
await projectBuildCache?.tryRestoreFromCacheAsync(terminal);
if (restoreFromCacheSuccess) {
return TaskStatus.FromCache;
}
}
if (this.isSkipAllowed && !buildCacheReadAttempted) {
const isPackageUnchanged: boolean = !!(
lastProjectDeps &&
projectDeps &&
projectDeps.arguments === lastProjectDeps.arguments &&
_areShallowEqual(projectDeps.files, lastProjectDeps.files)
);
if (isPackageUnchanged) {
return TaskStatus.Skipped;
}
}
// If the deps file exists, remove it before starting execution.
FileSystem.deleteFile(currentDepsPath);
// TODO: Remove legacyDepsPath with the next major release of Rush
const legacyDepsPath: string = path.join(this._rushProject.projectFolder, 'package-deps.json');
// Delete the legacy package-deps.json
FileSystem.deleteFile(legacyDepsPath);
if (!this._commandToRun) {
// Write deps on success.
if (projectDeps) {
JsonFile.save(projectDeps, currentDepsPath, {
ensureFolderExists: true
});
}
return TaskStatus.Success;
}
// Run the task
terminal.writeLine('Invoking: ' + this._commandToRun);
const task: child_process.ChildProcess = Utilities.executeLifecycleCommandAsync(this._commandToRun, {
rushConfiguration: this._rushConfiguration,
workingDirectory: projectFolder,
initCwd: this._rushConfiguration.commonTempFolder,
handleOutput: true,
environmentPathOptions: {
includeProjectBin: true
}
});
// Hook into events, in order to get live streaming of the log
if (task.stdout !== null) {
task.stdout.on('data', (data: Buffer) => {
const text: string = data.toString();
collatedTerminal.writeChunk({ text, kind: TerminalChunkKind.Stdout });
});
}
if (task.stderr !== null) {
task.stderr.on('data', (data: Buffer) => {
const text: string = data.toString();
collatedTerminal.writeChunk({ text, kind: TerminalChunkKind.Stderr });
hasWarningOrError = true;
});
}
let status: TaskStatus = await new Promise(
(resolve: (status: TaskStatus) => void, reject: (error: TaskError) => void) => {
task.on('close', (code: number) => {
try {
if (code !== 0) {
reject(new TaskError('error', `Returned error code: ${code}`));
} else if (hasWarningOrError) {
resolve(TaskStatus.SuccessWithWarning);
} else {
resolve(TaskStatus.Success);
}
} catch (error) {
reject(error as TaskError);
}
});
}
);
const taskIsSuccessful: boolean =
status === TaskStatus.Success ||
(status === TaskStatus.SuccessWithWarning &&
this.warningsAreAllowed &&
!!this._rushConfiguration.experimentsConfiguration.configuration
.buildCacheWithAllowWarningsInSuccessfulBuild);
if (taskIsSuccessful && projectDeps) {
// Write deps on success.
const writeProjectStatePromise: Promise<boolean> = JsonFile.saveAsync(projectDeps, currentDepsPath, {
ensureFolderExists: true
});
// If the command is successful, we can calculate project hash, and no dependencies were skipped,
// write a new cache entry.
const setCacheEntryPromise: Promise<boolean> | undefined = this.isCacheWriteAllowed
? (
await this._tryGetProjectBuildCacheAsync(
terminal,
trackedFiles,
context.repoCommandLineConfiguration
)
)?.trySetCacheEntryAsync(terminal)
: undefined;
const [, cacheWriteSuccess] = await Promise.all([writeProjectStatePromise, setCacheEntryPromise]);
if (terminalProvider.hasErrors) {
status = TaskStatus.Failure;
} else if (cacheWriteSuccess === false) {
status = TaskStatus.SuccessWithWarning;
}
}
normalizeNewlineTransform.close();
// If the pipeline is wired up correctly, then closing normalizeNewlineTransform should
// have closed projectLogWritable.
if (projectLogWritable.isOpen) {
throw new InternalError('The output file handle was not closed');
}
return status;
} finally {
projectLogWritable.close();
}
}