// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; using Microsoft.Build.Internal; using Microsoft.Build.Shared.FileSystem; namespace Microsoft.Build.Execution { /// /// Overall results for targets and requests /// public enum BuildResultCode { /// /// The target or request was a complete success. /// Success, /// /// The target or request failed in some way. /// Failure } /// /// Contains the current results for all of the targets which have produced results for a particular configuration. /// /// When modifying serialization/deserialization, bump the version and support previous versions in order to keep backwards compatible. public class BuildResult : BuildResultBase, INodePacket, IBuildResults { /// /// The submission with which this result is associated. /// private int _submissionId; /// /// The configuration ID with which this result is associated. /// private int _configurationId; /// /// The global build request ID for which these results are intended. /// private int _globalRequestId; /// /// The global build request ID which issued the request leading to this result. /// private int _parentGlobalRequestId; /// /// The build request ID on the originating node. /// private int _nodeRequestId; /// /// The first build request to generate results for a configuration will set this so that future /// requests may be properly satisfied from the cache. /// private List? _initialTargets; /// /// The first build request to generate results for a configuration will set this so that future /// requests may be properly satisfied from the cache. /// private List? _defaultTargets; /// /// The set of results for each target. /// private ConcurrentDictionary _resultsByTarget; /// /// Version of the build result. /// /// /// Allows to serialize and deserialize different versions of the build result. /// private int _version = Traits.Instance.EscapeHatches.DoNotVersionBuildResult ? 0 : 2; /// /// The request caused a circular dependency in scheduling. /// private bool _circularDependency; /// /// The exception generated while this request was running, if any. /// Note that this can be set if the request itself fails, or if it receives /// an exception from a target or task. /// private Exception? _requestException; /// /// The overall result calculated in the constructor. /// private bool _baseOverallResult = true; /// /// Snapshot of the environment from the configuration this results comes from. /// This should only be populated when the configuration for this result is moved between nodes. /// private FrozenDictionary? _savedEnvironmentVariables; /// /// When this key is in the dictionary , serialize the build result version. /// private const string SpecialKeyForVersion = "=MSBUILDFEATUREBUILDRESULTHASVERSION="; /// /// Set of additional keys tat might be added to the dictionary . /// private static readonly HashSet s_additionalEntriesKeys = new HashSet { SpecialKeyForVersion }; /// /// Snapshot of the current directory from the configuration this result comes from. /// This should only be populated when the configuration for this result is moved between nodes. /// private string? _savedCurrentDirectory; /// /// state after the build. This is only provided if /// includes or /// for the build request which this object is a result of, /// and will be null otherwise. Where available, it may be a non buildable-dummy object, and should only /// be used to retrieve , and /// from it. No other operation is guaranteed to be supported. /// private ProjectInstance? _projectStateAfterBuild; /// /// The flags provide additional control over the build results and may affect the cached value. /// /// /// Is optional, the field is expected to be present starting 1. /// private BuildRequestDataFlags _buildRequestDataFlags; /// /// The evaluation ID of the project used for this build. /// private int _evaluationId = BuildEventContext.InvalidEvaluationId; private string? _schedulerInducedError; private HashSet? _projectTargets; /// /// Constructor for serialization. /// public BuildResult() { _resultsByTarget = CreateTargetResultDictionary(1); } /// /// Constructor creates an empty build result /// /// The build request to which these results should be associated. internal BuildResult(BuildRequest request) : this(request, null) { } /// /// Constructs a build result with an exception /// /// The build request to which these results should be associated. /// The exception, if any. internal BuildResult(BuildRequest request, Exception? exception) : this(request, null, exception) { } /// /// Constructor creates a build result indicating a circular dependency was created. /// /// The build request to which these results should be associated. /// Set to true if a circular dependency was detected. internal BuildResult(BuildRequest request, bool circularDependency) : this(request, null) { _circularDependency = circularDependency; } /// /// Constructs a new build result based on existing results, but filtered by a specified set of target names /// /// The existing results. /// The target names whose results we will take from the existing results, if they exist. internal BuildResult(BuildResult existingResults, string[] targetNames) { _submissionId = existingResults._submissionId; _configurationId = existingResults._configurationId; _globalRequestId = existingResults._globalRequestId; _parentGlobalRequestId = existingResults._parentGlobalRequestId; _nodeRequestId = existingResults._nodeRequestId; _requestException = existingResults._requestException; _resultsByTarget = CreateTargetResultDictionaryWithContents(existingResults, targetNames); _baseOverallResult = existingResults.OverallResult == BuildResultCode.Success; _buildRequestDataFlags = existingResults._buildRequestDataFlags; _projectStateAfterBuild = existingResults._projectStateAfterBuild; _circularDependency = existingResults._circularDependency; } /// /// Constructs a new build result with existing results, but associated with the specified request. /// /// The build request with which these results should be associated. /// The existing results, if any. /// The exception, if any internal BuildResult(BuildRequest request, BuildResult? existingResults, Exception? exception) : this(request, existingResults, null, exception) { } /// /// Constructs a new build result with existing results, but associated with the specified request. /// /// The build request with which these results should be associated. /// The existing results, if any. /// The list of target names that are the subset of results that should be returned. /// The exception, if any internal BuildResult(BuildRequest request, BuildResult? existingResults, string[]? targetNames, Exception? exception) { _submissionId = request.SubmissionId; _configurationId = request.ConfigurationId; _globalRequestId = request.GlobalRequestId; _parentGlobalRequestId = request.ParentGlobalRequestId; _nodeRequestId = request.NodeRequestId; _circularDependency = false; _baseOverallResult = true; _buildRequestDataFlags = request.BuildRequestDataFlags; if (existingResults == null) { _requestException = exception; _resultsByTarget = CreateTargetResultDictionary(0); } else { _requestException = exception ?? existingResults._requestException; _resultsByTarget = targetNames == null ? existingResults._resultsByTarget : CreateTargetResultDictionaryWithContents(existingResults, targetNames); if (request.RequestedProjectState != null) { _projectStateAfterBuild = existingResults._projectStateAfterBuild?.FilteredCopy(request.RequestedProjectState); } } } /// /// Constructor which allows reporting results for a different nodeRequestId /// internal BuildResult(BuildResult result, int nodeRequestId) { _configurationId = result._configurationId; _globalRequestId = result._globalRequestId; _parentGlobalRequestId = result._parentGlobalRequestId; _nodeRequestId = nodeRequestId; _requestException = result._requestException; _resultsByTarget = result._resultsByTarget; _circularDependency = result._circularDependency; _initialTargets = result._initialTargets; _defaultTargets = result._defaultTargets; _projectTargets = result._projectTargets; _baseOverallResult = result.OverallResult == BuildResultCode.Success; } internal BuildResult(BuildResult result, int submissionId, int configurationId, int requestId, int parentRequestId, int nodeRequestId) { _submissionId = submissionId; _configurationId = configurationId; _globalRequestId = requestId; _parentGlobalRequestId = parentRequestId; _nodeRequestId = nodeRequestId; _requestException = result._requestException; _resultsByTarget = result._resultsByTarget; _circularDependency = result._circularDependency; _initialTargets = result._initialTargets; _defaultTargets = result._defaultTargets; _projectTargets = result._projectTargets; _baseOverallResult = result.OverallResult == BuildResultCode.Success; } /// /// Constructor for deserialization /// private BuildResult(ITranslator translator) { ((ITranslatable)this).Translate(translator); _resultsByTarget ??= CreateTargetResultDictionary(1); } /// /// Returns the submission id. /// public override int SubmissionId { [DebuggerStepThrough] get { return _submissionId; } } /// /// Returns the configuration ID for this result. /// public int ConfigurationId { [DebuggerStepThrough] get { return _configurationId; } } /// /// Returns the build request id for which this result was generated /// public int GlobalRequestId { [DebuggerStepThrough] get { return _globalRequestId; } } /// /// Returns the build request id for the parent of the request for which this result was generated /// public int ParentGlobalRequestId { [DebuggerStepThrough] get { return _parentGlobalRequestId; } } /// /// Returns the node build request id for which this result was generated /// public int NodeRequestId { [DebuggerStepThrough] get { return _nodeRequestId; } } /// /// Returns the exception generated while this result was run, if any. /// public override Exception? Exception { [DebuggerStepThrough] get { return _requestException; } [DebuggerStepThrough] internal set { _requestException = value; } } /// /// Returns a flag indicating if a circular dependency was detected. /// public override bool CircularDependency { [DebuggerStepThrough] get { return _circularDependency; } } /// /// Returns the overall result for this result set. /// public override BuildResultCode OverallResult { get { if (_requestException != null || _circularDependency || !_baseOverallResult) { return BuildResultCode.Failure; } foreach (KeyValuePair result in _resultsByTarget ?? []) { if ((result.Value.ResultCode == TargetResultCode.Failure && !result.Value.TargetFailureDoesntCauseBuildFailure) || result.Value.AfterTargetsHaveFailed) { return BuildResultCode.Failure; } } return BuildResultCode.Success; } } /// /// Returns an enumerator for all target results in this build result /// public IDictionary ResultsByTarget { [DebuggerStepThrough] get { return _resultsByTarget; } } /// /// state after the build. In general, it may be a non buildable-dummy object, and should only /// be used to retrieve , and /// from it. Any other operation is not guaranteed to be supported. /// public ProjectInstance? ProjectStateAfterBuild { get => _projectStateAfterBuild; set => _projectStateAfterBuild = value; } /// /// Gets the flags that were used in the build request to which these results are associated. /// See for examples of the available flags. /// /// /// Is optional, this property exists starting version 1. /// public BuildRequestDataFlags? BuildRequestDataFlags => (_version > 0) ? _buildRequestDataFlags : null; /// /// The evaluation ID of the project used for this build. /// internal int EvaluationId { [DebuggerStepThrough] get => _evaluationId; [DebuggerStepThrough] set => _evaluationId = value; } /// /// Returns the node packet type. /// NodePacketType INodePacket.Type { [DebuggerStepThrough] get { return NodePacketType.BuildResult; } } /// /// Holds a snapshot of the environment at the time we blocked. /// FrozenDictionary? IBuildResults.SavedEnvironmentVariables { get => _savedEnvironmentVariables; set => _savedEnvironmentVariables = value; } /// /// Holds a snapshot of the current working directory at the time we blocked. /// string? IBuildResults.SavedCurrentDirectory { get => _savedCurrentDirectory; set => _savedCurrentDirectory = value; } /// /// Returns the initial targets for the configuration which requested these results. /// internal List? InitialTargets { [DebuggerStepThrough] get { return _initialTargets; } [DebuggerStepThrough] set { _initialTargets = value; } } /// /// Returns the default targets for the configuration which requested these results. /// internal List? DefaultTargets { [DebuggerStepThrough] get { return _defaultTargets; } [DebuggerStepThrough] set { _defaultTargets = value; } } /// /// The defined targets for the project associated with this build result. /// internal HashSet? ProjectTargets { [DebuggerStepThrough] get => _projectTargets; [DebuggerStepThrough] set => _projectTargets = value; } /// /// Container used to transport errors from the scheduler (issued while computing a build result) /// to the TaskHost that has the proper logging context (project id, target id, task id, file location) /// internal string? SchedulerInducedError { get => _schedulerInducedError; set => _schedulerInducedError = value; } /// /// Indexer which sets or returns results for the specified target /// /// The target /// The results for the specified target /// KeyNotFoundException is returned if the specified target doesn't exist when reading this property. /// ArgumentException is returned if the specified target already has results. public ITargetResult this[string target] { [DebuggerStepThrough] get { return _resultsByTarget![target]; } } /// /// Adds the results for the specified target to this result collection. /// /// The target to which these results apply. /// The results for the target. public void AddResultsForTarget(string target, TargetResult result) { ArgumentNullException.ThrowIfNull(target); ArgumentNullException.ThrowIfNull(result); lock (this) { _resultsByTarget ??= CreateTargetResultDictionary(1); } if (_resultsByTarget.TryGetValue(target, out TargetResult? targetResult)) { Assumed.Equal(targetResult.ResultCode, TargetResultCode.Skipped, $"Items already exist for target {target}."); } _resultsByTarget[target] = result; } /// /// Keep the results only for targets in . /// /// The targets whose results to keep. internal void KeepSpecificTargetResults(IReadOnlyCollection targetsToKeep) { Assumed.Positive(targetsToKeep.Count, $"{nameof(targetsToKeep)} should contain at least one target."); foreach (string target in _resultsByTarget?.Keys ?? []) { if (!targetsToKeep.Contains(target)) { _ = _resultsByTarget!.TryRemove(target, out _); } } } /// /// Merges the specified results with the results contained herein. /// /// The results to merge in. public void MergeResults(BuildResult results) { ArgumentNullException.ThrowIfNull(results); Assumed.Equal(results.ConfigurationId, ConfigurationId, "Result configurations don't match"); // If we are merging with ourself or with a shallow clone, do nothing. if (ReferenceEquals(this, results) || ReferenceEquals(_resultsByTarget, results._resultsByTarget)) { return; } // Merge in the results foreach (KeyValuePair targetResult in results._resultsByTarget ?? []) { // NOTE: I believe that because we only allow results for a given target to be produced and cached once for a given configuration, // we can never receive conflicting results for that target, since the cache and build request manager would always return the // cached results after the first time the target is built. As such, we can allow "duplicates" to be merged in because there is // no change. If, however, this turns out not to be the case, we need to re-evaluate this merging and possibly re-enable the // assertion below. // Assumed.False(HasResultsForTarget(targetResult.Key), "Results already exist"); // Copy the new results in. _resultsByTarget![targetResult.Key] = targetResult.Value; } // If there is an exception and we did not previously have one, add it in. _requestException ??= results.Exception; } /// /// Determines if there are any results for the specified target. /// /// The target for which results are desired. /// True if results exist, false otherwise. public bool HasResultsForTarget(string target) { return _resultsByTarget?.ContainsKey(target) ?? false; } public bool TryGetResultsForTarget(string target, [NotNullWhen(true)] out TargetResult? value) { if (_resultsByTarget is null) { value = default; return false; } return _resultsByTarget.TryGetValue(target, out value); } #region INodePacket Members /// /// Reads or writes the packet to the serializer. /// void ITranslatable.Translate(ITranslator translator) { translator.Translate(ref _submissionId); translator.Translate(ref _configurationId); translator.Translate(ref _globalRequestId); translator.Translate(ref _parentGlobalRequestId); translator.Translate(ref _nodeRequestId); translator.Translate(ref _initialTargets); translator.Translate(ref _defaultTargets); translator.Translate(ref _projectTargets); translator.Translate(ref _circularDependency); translator.TranslateException(ref _requestException); translator.TranslateDictionary(ref _resultsByTarget, TargetResult.FactoryForDeserialization, CreateTargetResultDictionary); translator.Translate(ref _baseOverallResult); translator.Translate(ref _projectStateAfterBuild, ProjectInstance.FactoryForDeserialization); translator.Translate(ref _savedCurrentDirectory); translator.Translate(ref _schedulerInducedError); // This is a work-around for the bug https://github.com/dotnet/msbuild/issues/10208 // We are adding a version field to this class to make the ResultsCache backwards compatible with at least 2 previous releases. // The adding of a version field is done without a breaking change in 3 steps, each separated with at least 1 intermediate release. // // 1st step (done): Add a special key to the _savedEnvironmentVariables dictionary during the serialization. A workaround overload of the TranslateDictionary function is created to achieve it. // The presence of this key will indicate that the version is serialized next. // When serializing, add a key to the dictionary and serialize a version field. // Do not actually save the special key to dictionary during the deserialization, but read a version as a next field if it presents. // // 2nd step: Stop serialize a special key with the dictionary _savedEnvironmentVariables using the TranslateDictionary function workaround overload. Always serialize and de-serialize the version field. // Continue to deserialize _savedEnvironmentVariables with the TranslateDictionary function workaround overload in order not to deserialize dictionary with the special keys. // // 3rd step: Stop using the TranslateDictionary function workaround overload during _savedEnvironmentVariables deserialization. if (_version == 0) { // Escape hatch: serialize/deserialize without version field. translator.TranslateDictionary(ref _savedEnvironmentVariables, CommunicationsUtilities.EnvironmentVariableComparer); } else { IDictionary? savedEnvironmentVariables = _savedEnvironmentVariables; Dictionary additionalEntries = new(); if (translator.Mode == TranslationDirection.WriteToStream) { // Add the special key SpecialKeyForVersion to additional entries indicating the presence of a version to the _savedEnvironmentVariables dictionary. additionalEntries.Add(SpecialKeyForVersion, String.Empty); // Serialize the special key together with _savedEnvironmentVariables dictionary using the workaround overload of TranslateDictionary: translator.TranslateDictionary(ref savedEnvironmentVariables, CommunicationsUtilities.EnvironmentVariableComparer, ref additionalEntries, s_additionalEntriesKeys); // Serialize version translator.Translate(ref _version); } else if (translator.Mode == TranslationDirection.ReadFromStream) { // Read the dictionary using the workaround overload of TranslateDictionary: special keys (additionalEntriesKeys) would be read to additionalEntries instead of the _savedEnvironmentVariables dictionary. translator.TranslateDictionary(ref savedEnvironmentVariables, CommunicationsUtilities.EnvironmentVariableComparer, ref additionalEntries, s_additionalEntriesKeys); _savedEnvironmentVariables = savedEnvironmentVariables? .ToFrozenDictionary(CommunicationsUtilities.EnvironmentVariableComparer); // no-op if already frozen // If the special key SpecialKeyForVersion present in additionalEntries, also read a version, otherwise set it to 0. if (additionalEntries is not null && additionalEntries.ContainsKey(SpecialKeyForVersion)) { translator.Translate(ref _version); } else { _version = 0; } } } // Starting version 1 this _buildRequestDataFlags field is present. if (_version > 0) { translator.TranslateEnum(ref _buildRequestDataFlags, (int)_buildRequestDataFlags); } // Starting version 2 the _evaluationId field is present. if (_version >= 2) { translator.Translate(ref _evaluationId); } } /// /// Factory for serialization /// internal static BuildResult FactoryForDeserialization(ITranslator translator) { return new BuildResult(translator); } #endregion /// /// Caches all of the targets results we can. /// internal void CacheIfPossible() { foreach (KeyValuePair targetResultPair in _resultsByTarget ?? []) { targetResultPair.Value.CacheItems(ConfigurationId, targetResultPair.Key); } } /// /// Clear cached files from disk. /// internal void ClearCachedFiles() { string resultsDirectory = TargetResult.GetCacheDirectory(_configurationId, "None" /*Does not matter because we just need the directory name not the file*/); if (FileSystems.Default.DirectoryExists(resultsDirectory)) { FileUtilities.DeleteDirectoryNoThrow(resultsDirectory, true /*recursive*/); } } /// /// Clones the build result (the resultsByTarget field is only a shallow copy). /// internal BuildResult Clone() { BuildResult result = new BuildResult { _submissionId = _submissionId, _configurationId = _configurationId, _globalRequestId = _globalRequestId, _parentGlobalRequestId = _parentGlobalRequestId, _nodeRequestId = _nodeRequestId, _requestException = _requestException, _resultsByTarget = new ConcurrentDictionary(_resultsByTarget, StringComparer.OrdinalIgnoreCase), _baseOverallResult = OverallResult == BuildResultCode.Success, _circularDependency = _circularDependency }; return result; } /// /// Sets the overall result. /// /// true if the result is success, otherwise false. internal void SetOverallResult(bool overallResult) { _baseOverallResult = false; } /// /// Creates the target result dictionary. /// private static ConcurrentDictionary CreateTargetResultDictionary(int capacity) { return new ConcurrentDictionary(1, capacity, StringComparer.OrdinalIgnoreCase); } /// /// Creates the target result dictionary and populates it with however many target results are /// available given the list of targets passed. /// private static ConcurrentDictionary CreateTargetResultDictionaryWithContents(BuildResult existingResults, string[] targetNames) { ConcurrentDictionary resultsByTarget = CreateTargetResultDictionary(targetNames.Length); foreach (string target in targetNames) { if (existingResults.ResultsByTarget?.TryGetValue(target, out TargetResult? targetResult) ?? false) { resultsByTarget[target] = targetResult; } } return resultsByTarget; } } }