File: PlatformNegotiation.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// 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.Generic;
using Microsoft.Build.Framework;
#if BUILD_ENGINE
using Microsoft.Build.BackEnd;
#else
using Microsoft.Build.Utilities;
#endif
 
namespace Microsoft.Build.Shared
{
    /// <summary>
    /// This class contains only static methods, which are used in both the
    /// tasks and graph projects in order for two projects to negotiate which platform a projectreference
    /// should be built as.
    /// </summary>
    internal static class PlatformNegotiation
    {
        internal static string GetNearestPlatform(string overridePlatformValue, string referencedProjectPlatform, string projectReferencePlatformsMetadata, string projectReferenceLookupTableMetadata, string platformLookupTable, string projectPath, string currentProjectPlatform, TaskLoggingHelper? log = null)
        {
            Dictionary<string, string>? currentProjectLookupTable = ExtractLookupTable(platformLookupTable, log);
 
            if (string.IsNullOrEmpty(projectReferencePlatformsMetadata) && string.IsNullOrEmpty(referencedProjectPlatform))
            {
                log?.LogWarningWithCodeFromResources("GetCompatiblePlatform.NoPlatformsListed", projectPath);
                return string.Empty;
            }
 
            // Pull platformLookupTable metadata from the referenced project. This allows custom
            // mappings on a per-ProjectReference basis.
            Dictionary<string, string>? projectReferenceLookupTable = ExtractLookupTable(projectReferenceLookupTableMetadata, log);
 
            HashSet<string> projectReferencePlatforms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (string s in projectReferencePlatformsMetadata.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries))
            {
                projectReferencePlatforms.Add(s);
            }
 
            string buildProjectReferenceAs = string.Empty;
 
            // If an override value is set define that as the platform value as the top priority
            if (!string.IsNullOrEmpty(overridePlatformValue))
            {
                buildProjectReferenceAs = overridePlatformValue;
            }
            // If the referenced project has a defined `Platform` that's compatible, it will build that way by default.
            // Don't set `buildProjectReferenceAs` and the `_GetProjectReferencePlatformProperties` target will handle the rest.
            else if (!string.IsNullOrEmpty(referencedProjectPlatform) && referencedProjectPlatform.Equals(currentProjectPlatform, StringComparison.OrdinalIgnoreCase))
            {
                log?.LogMessageFromResources(MessageImportance.Low, "GetCompatiblePlatform.ReferencedProjectHasDefinitivePlatform", projectPath, referencedProjectPlatform);
            }
            // Prefer matching platforms
            else if (projectReferencePlatforms.Contains(currentProjectPlatform))
            {
                buildProjectReferenceAs = currentProjectPlatform;
                log?.LogMessageFromResources(MessageImportance.Low, "GetCompatiblePlatform.SamePlatform");
            }
            // Prioritize platformLookupTable **metadata** attached to the ProjectReference item
            // before the current project's table. We do this to allow per-ProjectReference fine tuning.
            else if (projectReferenceLookupTable != null &&
                    projectReferenceLookupTable.ContainsKey(currentProjectPlatform) &&
                    projectReferencePlatforms.Contains(projectReferenceLookupTable[currentProjectPlatform]))
            {
                buildProjectReferenceAs = projectReferenceLookupTable[currentProjectPlatform];
                log?.LogMessageFromResources(MessageImportance.Low, "GetCompatiblePlatform.FoundMappingInTable", currentProjectPlatform, buildProjectReferenceAs, projectReferenceLookupTableMetadata);
            }
            // Current project's translation table follows
            else if (currentProjectLookupTable != null &&
                    currentProjectLookupTable.ContainsKey(currentProjectPlatform) &&
                    projectReferencePlatforms.Contains(currentProjectLookupTable[currentProjectPlatform]))
            {
                buildProjectReferenceAs = currentProjectLookupTable[currentProjectPlatform];
                log?.LogMessageFromResources(MessageImportance.Low, "GetCompatiblePlatform.FoundMappingInTable", currentProjectPlatform, buildProjectReferenceAs, platformLookupTable);
            }
            // AnyCPU if possible
            else if (projectReferencePlatforms.Contains("AnyCPU"))
            {
                buildProjectReferenceAs = "AnyCPU";
                log?.LogMessageFromResources(MessageImportance.Low, "GetCompatiblePlatform.AnyCPUDefault");
            }
            else
            {
                // Keep NearestPlatform empty, log a warning. Common.CurrentVersion.targets will undefine
                // Platform/PlatformTarget when this is the case.
                log?.LogWarningWithCodeFromResources("GetCompatiblePlatform.NoCompatiblePlatformFound", projectPath);
            }
            // If the referenced project has a defined `Platform` that's compatible, it will build that way by default.
            // If we're about to tell the reference to build using its default platform, don't pass it as a global property.
            if (!string.IsNullOrEmpty(referencedProjectPlatform) && referencedProjectPlatform.Equals(buildProjectReferenceAs, StringComparison.OrdinalIgnoreCase))
            {
                log?.LogMessageFromResources(MessageImportance.Low, "GetCompatiblePlatform.ReferencedProjectHasDefinitivePlatform", projectPath, referencedProjectPlatform);
                buildProjectReferenceAs = string.Empty;
            }
            return buildProjectReferenceAs;
        }
        internal static Dictionary<string, string>? ExtractLookupTable(string stringTable, TaskLoggingHelper? log = null)
        {
            if (string.IsNullOrEmpty(stringTable))
            {
                return null;
            }
 
            Dictionary<string, string> table = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
            foreach (string s in stringTable.Trim().Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries))
            {
                string[] keyVal = s.Trim().Split(MSBuildConstants.EqualsChar);
 
                // Invalid table, don't use it.
                if (keyVal.Length != 2 || string.IsNullOrEmpty(keyVal[0]) || string.IsNullOrEmpty(keyVal[1]))
                {
                    log?.LogWarningWithCodeFromResources("GetCompatiblePlatform.InvalidLookupTableFormat", stringTable);
                    return null;
                }
 
                table[keyVal[0]] = keyVal[1];
            }
 
            log?.LogMessageFromResources(MessageImportance.Low, "GetCompatiblePlatform.LookupTableParsed", stringTable);
 
            return table;
        }
    }
}