diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/CallTarget.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/CallTarget.cs index ef8c7ff39a3..681101088c1 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/CallTarget.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/CallTarget.cs @@ -114,7 +114,8 @@ public Task ExecuteInternal() targetOutputs: _targetOutputs, unloadProjectsOnCompletion: false, toolsVersion: null, - skipNonexistentTargets: false); + skipNonexistentTargets: false, + targetAndPropertyListSeparators: null); } #endregion diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs index db7e94103fe..e4538c5f54c 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs @@ -206,6 +206,8 @@ public string SkipNonexistentProjects /// public string[] TargetAndPropertyListSeparators { get; set; } = null; + private static string[] SemicolonSeparatorArray = [";"]; + /// /// If set, MSBuild will skip the targets specified in this build request if they are not defined in the /// to build. This only applies to this build request (if another target calls the @@ -257,11 +259,10 @@ public async Task ExecuteInternal() } // Parse out the properties to undefine, if any. - string[] undefinePropertiesArray = null; - if (!String.IsNullOrEmpty(RemoveProperties)) + string[] undefinePropertiesArray = ExpandItemList(TargetAndPropertyListSeparators ?? SemicolonSeparatorArray, RemoveProperties); + if (undefinePropertiesArray.Length > 0) { Log.LogMessageFromResources(MessageImportance.Low, "General.UndefineProperties"); - undefinePropertiesArray = RemoveProperties.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries); foreach (string property in undefinePropertiesArray) { Log.LogMessageFromText($" {property}", MessageImportance.Low); @@ -374,7 +375,8 @@ public async Task ExecuteInternal() _targetOutputs, UnloadProjectsOnCompletion, ToolsVersion, - SkipNonexistentTargets); + SkipNonexistentTargets, + TargetAndPropertyListSeparators); if (!executeResult) { @@ -445,7 +447,8 @@ private async Task BuildProjectsInParallel(Dictionary prop _targetOutputs, UnloadProjectsOnCompletion, ToolsVersion, - SkipNonexistentTargets); + SkipNonexistentTargets, + TargetAndPropertyListSeparators); if (!executeResult) { @@ -455,6 +458,19 @@ private async Task BuildProjectsInParallel(Dictionary prop return success; } + /// + /// Expand a single item list into an array of strings - intended for use on scalar values that represent lists of items (e.g. Properties lists on an ITaskItem metadata) + /// + private static string[] ExpandItemList(string[] separators, string itemList) + { + if (string.IsNullOrEmpty(itemList)) + { + return Array.Empty(); + } + + return itemList.Split(separators, StringSplitOptions.RemoveEmptyEntries); + } + /// /// Expand and re-construct arrays of all targets and properties /// @@ -469,7 +485,7 @@ private void ExpandAllTargetsAndProperties() foreach (string p in Properties) { // Split each property according to the separators - string[] expandedPropertyValues = p.Split(TargetAndPropertyListSeparators, StringSplitOptions.RemoveEmptyEntries); + string[] expandedPropertyValues = ExpandItemList(TargetAndPropertyListSeparators ?? SemicolonSeparatorArray, p); // Add the resultant properties to the final list foreach (string property in expandedPropertyValues) { @@ -486,7 +502,7 @@ private void ExpandAllTargetsAndProperties() foreach (string t in Targets) { // Split each target according to the separators - string[] expandedTargetValues = t.Split(TargetAndPropertyListSeparators, StringSplitOptions.RemoveEmptyEntries); + string[] expandedTargetValues = ExpandItemList(TargetAndPropertyListSeparators ?? SemicolonSeparatorArray, t); // Add the resultant targets to the final list foreach (string target in expandedTargetValues) { @@ -537,7 +553,8 @@ internal static async Task ExecuteTargets( List targetOutputs, bool unloadProjectsOnCompletion, string toolsVersion, - bool skipNonexistentTargets) + bool skipNonexistentTargets, + string[] targetAndPropertyListSeparators) { bool success = true; @@ -563,15 +580,16 @@ internal static async Task ExecuteTargets( projectNames[i] = projects[i].ItemSpec; toolsVersions[i] = toolsVersion; + string[] projectSpecificPropertiesMetadata = ExpandItemList(targetAndPropertyListSeparators ?? SemicolonSeparatorArray, projects[i].GetMetadata(ItemMetadataNames.PropertiesMetadataName)); // If the user specified a different set of global properties for this project, then // parse the string containing the properties - if (!String.IsNullOrEmpty(projects[i].GetMetadata(ItemMetadataNames.PropertiesMetadataName))) + if (projectSpecificPropertiesMetadata.Length > 0) { if (!PropertyParser.GetTableWithEscaping( log, ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.OverridingProperties", projectNames[i]), ItemMetadataNames.PropertiesMetadataName, - projects[i].GetMetadata(ItemMetadataNames.PropertiesMetadataName).Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries), + projectSpecificPropertiesMetadata, out Dictionary preProjectPropertiesTable)) { return false; @@ -582,46 +600,51 @@ internal static async Task ExecuteTargets( if (undefineProperties != null) { - undefinePropertiesPerProject[i] = new List(undefineProperties); + undefinePropertiesPerProject[i] = [.. undefineProperties]; } // If the user wanted to undefine specific global properties for this project, parse // that string and remove them now. - string projectUndefineProperties = projects[i].GetMetadata(ItemMetadataNames.UndefinePropertiesMetadataName); - if (!String.IsNullOrEmpty(projectUndefineProperties)) + string[] projectSpecificUndefinePropertiesMetadata = ExpandItemList(targetAndPropertyListSeparators ?? SemicolonSeparatorArray, projects[i].GetMetadata(ItemMetadataNames.UndefinePropertiesMetadataName)); + if (projectSpecificUndefinePropertiesMetadata.Length > 0) { - string[] propertiesToUndefine = projectUndefineProperties.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries); - if (undefinePropertiesPerProject[i] == null) + if (undefinePropertiesPerProject[i] is null) + { + undefinePropertiesPerProject[i] = [.. projectSpecificUndefinePropertiesMetadata]; + } + else { - undefinePropertiesPerProject[i] = new List(propertiesToUndefine.Length); + undefinePropertiesPerProject[i].AddRange(projectSpecificUndefinePropertiesMetadata); } - if (log != null && propertiesToUndefine.Length > 0) + if (log != null) { log.LogMessageFromResources(MessageImportance.Low, "General.ProjectUndefineProperties", projectNames[i]); - foreach (string property in propertiesToUndefine) + foreach (string property in projectSpecificUndefinePropertiesMetadata) { - undefinePropertiesPerProject[i].Add(property); log.LogMessageFromText($" {property}", MessageImportance.Low); } } } + string[] projectSpecificAdditionalPropertiesMetadata = ExpandItemList(targetAndPropertyListSeparators ?? SemicolonSeparatorArray, projects[i].GetMetadata(ItemMetadataNames.AdditionalPropertiesMetadataName)); // If the user specified a different set of global properties for this project, then // parse the string containing the properties - if (!String.IsNullOrEmpty(projects[i].GetMetadata(ItemMetadataNames.AdditionalPropertiesMetadataName))) + if (projectSpecificAdditionalPropertiesMetadata.Length > 0) { if (!PropertyParser.GetTableWithEscaping( log, ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.AdditionalProperties", projectNames[i]), ItemMetadataNames.AdditionalPropertiesMetadataName, - projects[i].GetMetadata(ItemMetadataNames.AdditionalPropertiesMetadataName).Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries), + projectSpecificAdditionalPropertiesMetadata, out Dictionary additionalProjectPropertiesTable)) { return false; } - var combinedTable = new Dictionary(StringComparer.OrdinalIgnoreCase); + // attempt to pre-size dictionaries - worst case we need to hold all entries from both, so assume that since huge amounts of properties are uncommon. + var combinedTable = new Dictionary((projectProperties[i]?.Count ?? 0) + additionalProjectPropertiesTable.Count, StringComparer.OrdinalIgnoreCase); + // todo(chusk): would it be faster to just clone the projectProperties[i] dictionary as a base, and add additionalProjectPropertiesTable on top of it? // First copy in the properties from the global table that not in the additional properties table if (projectProperties[i] != null) { @@ -642,7 +665,7 @@ internal static async Task ExecuteTargets( } // If the user specified a different toolsVersion for this project - then override the setting - if (!String.IsNullOrEmpty(projects[i].GetMetadata("ToolsVersion"))) + if (!string.IsNullOrEmpty(projects[i].GetMetadata("ToolsVersion"))) { toolsVersions[i] = projects[i].GetMetadata("ToolsVersion"); } diff --git a/src/Tasks/CallTarget.cs b/src/Tasks/CallTarget.cs index 74acb20e7c3..bc60602a4e1 100644 --- a/src/Tasks/CallTarget.cs +++ b/src/Tasks/CallTarget.cs @@ -77,17 +77,18 @@ public override bool Execute() // Build the specified targets in the current project. return MSBuild.ExecuteTargets( - singleProject, // project = null (current project) - null, // propertiesTable = null - null, // undefineProperties - targetLists, // list of targets to build - false, // stopOnFirstFailure = false - false, // rebaseOutputs = false - BuildEngine3, - Log, - _targetOutputs, - false, - null); // toolsVersion = null + projects: singleProject, + propertiesTable: null, + undefineProperties: null, + targetLists: targetLists, + stopOnFirstFailure: false, + rebaseOutputs: false, + buildEngine: BuildEngine3, + log: Log, + targetOutputs: _targetOutputs, + unloadProjectsOnCompletion: false, + toolsVersion: null, + targetAndPropertyListSeparators: null); } #endregion diff --git a/src/Tasks/MSBuild.cs b/src/Tasks/MSBuild.cs index df5ca14e163..53d8295044e 100644 --- a/src/Tasks/MSBuild.cs +++ b/src/Tasks/MSBuild.cs @@ -58,19 +58,16 @@ private enum SkipNonExistentProjectsBehavior /// /// A list of property name/value pairs to apply as global properties to /// the child project. - /// A typical input: "propname1=propvalue1", "propname2=propvalue2", "propname3=propvalue3". + /// A typical input would look like this: + /// Properties="propname1=propvalue1;propname2=propvalue2;propname3=propvalue3" /// /// - /// ` - /// The engine fails on this because it doesn't like item lists being concatenated with string - /// constants when the data is being passed into an array parameter. So the workaround is to - /// write this in the project file: - /// `` - /// ]]> - /// + /// The fact that this is a string[] makes the following illegal: + /// ]]> + /// The engine fails on this because it doesn't like item lists being concatenated with string + /// constants when the data is being passed into an array parameter. So the workaround is to + /// write this in the project file: + /// ]]> /// public string[] Properties { get; set; } @@ -187,6 +184,8 @@ public string SkipNonexistentProjects /// public string[] TargetAndPropertyListSeparators { get; set; } + private static string[] SemicolonSeparatorArray = [";"]; + #endregion #region ITask Members @@ -217,11 +216,10 @@ public override bool Execute() } // Parse out the properties to undefine, if any. - string[] undefinePropertiesArray = null; - if (!String.IsNullOrEmpty(RemoveProperties)) + string[] undefinePropertiesArray = ExpandItemList(TargetAndPropertyListSeparators ?? SemicolonSeparatorArray, RemoveProperties); + if (undefinePropertiesArray.Length > 0) { Log.LogMessageFromResources(MessageImportance.Low, "General.UndefineProperties"); - undefinePropertiesArray = RemoveProperties.Split(MSBuildConstants.SemicolonChar); foreach (string property in undefinePropertiesArray) { Log.LogMessageFromText($" {property}", MessageImportance.Low); @@ -334,7 +332,8 @@ public override bool Execute() Log, _targetOutputs, UnloadProjectsOnCompletion, - ToolsVersion)) + ToolsVersion, + TargetAndPropertyListSeparators)) { success = false; } @@ -398,7 +397,8 @@ private bool BuildProjectsInParallel(Dictionary propertiesTable, Log, _targetOutputs, UnloadProjectsOnCompletion, - ToolsVersion)) + ToolsVersion, + TargetAndPropertyListSeparators)) { success = false; } @@ -406,6 +406,19 @@ private bool BuildProjectsInParallel(Dictionary propertiesTable, return success; } + /// + /// Expand a single item list into an array of strings - intended for use on scalar values that represent lists of items (e.g. Properties lists on an ITaskItem metadata) + /// + private static string[] ExpandItemList(string[] separators, string itemList) + { + if (string.IsNullOrEmpty(itemList)) + { + return Array.Empty(); + } + + return itemList.Split(separators, StringSplitOptions.RemoveEmptyEntries); + } + /// /// Expand and re-construct arrays of all targets and properties /// @@ -420,7 +433,7 @@ private void ExpandAllTargetsAndProperties() foreach (string t in Properties) { // Split each property according to the separators - string[] expandedPropertyValues = t.Split(TargetAndPropertyListSeparators, StringSplitOptions.RemoveEmptyEntries); + string[] expandedPropertyValues = ExpandItemList(TargetAndPropertyListSeparators ?? SemicolonSeparatorArray, t); // Add the resultant properties to the final list foreach (string property in expandedPropertyValues) @@ -438,7 +451,7 @@ private void ExpandAllTargetsAndProperties() foreach (string t in Targets) { // Split each target according to the separators - string[] expandedTargetValues = t.Split(TargetAndPropertyListSeparators, StringSplitOptions.RemoveEmptyEntries); + string[] expandedTargetValues = ExpandItemList(TargetAndPropertyListSeparators ?? SemicolonSeparatorArray, t); // Add the resultant targets to the final list foreach (string target in expandedTargetValues) @@ -488,7 +501,8 @@ internal static bool ExecuteTargets( TaskLoggingHelper log, List targetOutputs, bool unloadProjectsOnCompletion, - string toolsVersion) + string toolsVersion, + string[] targetAndPropertyListSeparators) { bool success = true; @@ -499,7 +513,7 @@ internal static bool ExecuteTargets( var projectNames = new string[projects.Count]; var toolsVersions = new string[projects.Count]; var projectProperties = new Dictionary[projects.Count]; - var undefinePropertiesPerProject = new IList[projects.Count]; + var undefinePropertiesPerProject = new List[projects.Count]; for (int i = 0; i < projectNames.Length; i++) { @@ -516,11 +530,15 @@ internal static bool ExecuteTargets( // If the user specified a different set of global properties for this project, then // parse the string containing the properties - if (!String.IsNullOrEmpty(projects[i].GetMetadata("Properties"))) + string[] projectSpecificPropertiesMetadata = ExpandItemList(targetAndPropertyListSeparators ?? SemicolonSeparatorArray, projects[i].GetMetadata("Properties")); + if (projectSpecificPropertiesMetadata.Length > 0) { if (!PropertyParser.GetTableWithEscaping( - log, ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.OverridingProperties", projectNames[i]), "Properties", projects[i].GetMetadata("Properties").Split(MSBuildConstants.SemicolonChar), - out Dictionary preProjectPropertiesTable)) + log, + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.OverridingProperties", projectNames[i]), + "Properties", + projectSpecificPropertiesMetadata, + out Dictionary preProjectPropertiesTable)) { return false; } @@ -529,26 +547,28 @@ internal static bool ExecuteTargets( if (undefineProperties != null) { - undefinePropertiesPerProject[i] = new List(undefineProperties); + undefinePropertiesPerProject[i] = [.. undefineProperties]; } // If the user wanted to undefine specific global properties for this project, parse // that string and remove them now. - string projectUndefineProperties = projects[i].GetMetadata("UndefineProperties"); - if (!String.IsNullOrEmpty(projectUndefineProperties)) + string[] projectSpecificUndefinePropertiesMetadata = ExpandItemList(targetAndPropertyListSeparators ?? SemicolonSeparatorArray, projects[i].GetMetadata("UndefineProperties")); + if (projectSpecificUndefinePropertiesMetadata.Length > 0) { - string[] propertiesToUndefine = projectUndefineProperties.Split(MSBuildConstants.SemicolonChar); if (undefinePropertiesPerProject[i] == null) { - undefinePropertiesPerProject[i] = new List(propertiesToUndefine.Length); + undefinePropertiesPerProject[i] = [.. projectSpecificUndefinePropertiesMetadata]; + } + else + { + undefinePropertiesPerProject[i].AddRange(projectSpecificUndefinePropertiesMetadata); } - if (log != null && propertiesToUndefine.Length > 0) + if (log != null) { log.LogMessageFromResources(MessageImportance.Low, "General.ProjectUndefineProperties", projectNames[i]); - foreach (string property in propertiesToUndefine) + foreach (string property in projectSpecificUndefinePropertiesMetadata) { - undefinePropertiesPerProject[i].Add(property); log.LogMessageFromText($" {property}", MessageImportance.Low); } } @@ -556,15 +576,22 @@ internal static bool ExecuteTargets( // If the user specified a different set of global properties for this project, then // parse the string containing the properties - if (!String.IsNullOrEmpty(projects[i].GetMetadata("AdditionalProperties"))) + string[] projectSpecificAdditionalPropertiesMetadata = ExpandItemList(targetAndPropertyListSeparators ?? SemicolonSeparatorArray, projects[i].GetMetadata("AdditionalProperties")); + if (projectSpecificAdditionalPropertiesMetadata.Length > 0) { if (!PropertyParser.GetTableWithEscaping( - log, ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.AdditionalProperties", projectNames[i]), "AdditionalProperties", projects[i].GetMetadata("AdditionalProperties").Split(MSBuildConstants.SemicolonChar), - out Dictionary additionalProjectPropertiesTable)) + log, + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.AdditionalProperties", projectNames[i]), + "AdditionalProperties", + projectSpecificAdditionalPropertiesMetadata, + out Dictionary additionalProjectPropertiesTable)) { return false; } - var combinedTable = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // attempt to pre-size dictionaries - worst case we need to hold all entries from both, so assume that since huge amounts of properties are uncommon. + var combinedTable = new Dictionary((projectProperties[i]?.Count ?? 0) + additionalProjectPropertiesTable.Count, StringComparer.OrdinalIgnoreCase); + // todo(chusk): would it be faster to just clone the projectProperties[i] dictionary as a base, and add additionalProjectPropertiesTable on top of it? // First copy in the properties from the global table that not in the additional properties table if (projectProperties[i] != null) { diff --git a/src/Tasks/Microsoft.Common.CurrentVersion.targets b/src/Tasks/Microsoft.Common.CurrentVersion.targets index 5c63581f44c..8aceefca680 100644 --- a/src/Tasks/Microsoft.Common.CurrentVersion.targets +++ b/src/Tasks/Microsoft.Common.CurrentVersion.targets @@ -2130,58 +2130,70 @@ Copyright (C) Microsoft Corporation. All rights reserved. much information as possible will be passed to the compilers. --> + + <_ProjectsForGetTargetPath Include="@(_MSBuildProjectReferenceExistent)" + Condition="'%(_MSBuildProjectReferenceExistent.BuildReference)' == 'true' and '@(ProjectReferenceWithConfiguration)' != '' and ('$(BuildingInsideVisualStudio)' == 'true' or '$(BuildProjectReferences)' != 'true') and '$(VisualStudioVersion)' != '10.0' and '@(_MSBuildProjectReferenceExistent)' != ''" + Properties="%(_MSBuildProjectReferenceExistent.SetConfiguration);%(_MSBuildProjectReferenceExistent.SetPlatform);%(_MSBuildProjectReferenceExistent.SetTargetFramework)" + RemoveProperties="%(_MSBuildProjectReferenceExistent.GlobalPropertiesToRemove);$(_GlobalPropertiesToRemoveFromProjectReferences)" /> + + - + ContinueOnError="!$(BuildingProject)"> + Condition="'%(_ProjectsForGetTargetPath.ReferenceOutputAssembly)'=='true'" /> - + ItemName="%(_ProjectsForGetTargetPath.OutputItemType)" + Condition="'%(_ProjectsForGetTargetPath.OutputItemType)' != ''" /> - + <_ProjectsForCommandLineBuild Include="@(_MSBuildProjectReferenceExistent)" Condition="'%(_MSBuildProjectReferenceExistent.BuildReference)' == 'true' and '@(ProjectReferenceWithConfiguration)' != '' and '$(BuildingInsideVisualStudio)' != 'true' and '$(BuildProjectReferences)' == 'true' and '@(_MSBuildProjectReferenceExistent)' != ''" - ContinueOnError="$(ContinueOnError)" - RemoveProperties="%(_MSBuildProjectReferenceExistent.GlobalPropertiesToRemove)$(_GlobalPropertiesToRemoveFromProjectReferences)"> + Properties="%(_MSBuildProjectReferenceExistent.SetConfiguration);%(_MSBuildProjectReferenceExistent.SetPlatform);%(_MSBuildProjectReferenceExistent.SetTargetFramework)" + RemoveProperties="%(_MSBuildProjectReferenceExistent.GlobalPropertiesToRemove);$(_GlobalPropertiesToRemoveFromProjectReferences)" + Targets="%(_MSBuildProjectReferenceExistent.Targets)" /> + + + + Condition="'%(_ProjectsForCommandLineBuild.ReferenceOutputAssembly)'=='true' or '$(DesignTimeBuild)' == 'true'" /> + ItemName="%(_ProjectsForCommandLineBuild.OutputItemType)" + Condition="'%(_ProjectsForCommandLineBuild.OutputItemType)' != ''" /> + + <_ProjectsForNativeManifest Include="@(_MSBuildProjectReferenceExistent)" + Condition="'%(_MSBuildProjectReferenceExistent.BuildReference)' == 'true' and '@(ProjectReferenceWithConfiguration)' != '' and '$(BuildingProject)' == 'true' and '@(_MSBuildProjectReferenceExistent)' != ''" + Properties="%(_MSBuildProjectReferenceExistent.SetConfiguration);%(_MSBuildProjectReferenceExistent.SetPlatform);%(_MSBuildProjectReferenceExistent.SetTargetFramework)" + RemoveProperties="%(_MSBuildProjectReferenceExistent.GlobalPropertiesToRemove);$(_GlobalPropertiesToRemoveFromProjectReferences)" /> + + + SkipNonexistentTargets="true"> + Condition="'%(_ProjectsForNativeManifest.ReferenceOutputAssembly)' == 'true'" />