private async _executeTaskAsync()

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();
    }
  }