| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. |
| 2 | |
| 3 | namespace WixToolset.BuildTasks |
| 4 | { |
| 5 | using System; |
| 6 | using System.Collections.Generic; |
| 7 | using System.IO; |
| 8 | using System.Linq; |
| 9 | using System.Text; |
| 10 | using Microsoft.Build.Framework; |
| 11 | using Microsoft.Build.Utilities; |
| 12 | |
| 13 | /// <summary> |
| 14 | /// This task adds mutli-targeting and publish metadata to the appropriate project references. |
| 15 | /// </summary> |
| 16 | public class UpdateProjectReferenceMetadata : Task |
| 17 | { |
| 18 | private static readonly char[] MetadataPairSplitter = new char[] { '|' }; |
| 19 | |
| 20 | /// <summary> |
| 21 | /// The list of project references that exist. |
| 22 | /// </summary> |
| 23 | [Required] |
| 24 | public ITaskItem[] ProjectReferences { get; set; } |
| 25 | |
| 26 | [Required] |
| 27 | public string IntermediateFolder { get; set; } |
| 28 | |
| 29 | [Output] |
| 30 | public ITaskItem[] UpdatedProjectReferences { get; private set; } |
| 31 | |
| 32 | /// <summary> |
| 33 | /// Finds all project references requesting multi-targeting and publishing and updates them to publish instead of build and |
| 34 | /// sets target framework if requested. |
| 35 | /// </summary> |
| 36 | /// <returns>True upon completion of the task execution.</returns> |
| 37 | public override bool Execute() |
| 38 | { |
| 39 | var intermediateFolder = Path.GetFullPath(this.IntermediateFolder); |
| 40 | |
| 41 | // Create the project reference facades. |
| 42 | var projectReferenceFacades = this.ProjectReferences.Select(p => ProjectReferenceFacade.CreateFacade(p, this.Log, intermediateFolder)); |
| 43 | |
| 44 | // Expand the facade count by applying Configurations/Platforms. |
| 45 | projectReferenceFacades = this.ExpandProjectReferencesForConfigurationsAndPlatforms(projectReferenceFacades); |
| 46 | |
| 47 | // Expand the facade count by applying TargetFrameworks/RuntimeIdentifiers. |
| 48 | projectReferenceFacades = this.ExpandProjectReferencesForTargetFrameworksAndRuntimeIdentifiers(projectReferenceFacades); |
| 49 | |
| 50 | // Assign any metadata added during expansion above to the project references. |
| 51 | this.UpdatedProjectReferences = this.AssignMetadataToProjectReferences(projectReferenceFacades).ToArray(); |
| 52 | |
| 53 | return true; |
| 54 | } |
| 55 | |
| 56 | private IEnumerable<ProjectReferenceFacade> ExpandProjectReferencesForConfigurationsAndPlatforms(IEnumerable<ProjectReferenceFacade> projectReferenceFacades) |
| 57 | { |
| 58 | foreach (var projectReferenceFacade in projectReferenceFacades) |
| 59 | { |
| 60 | var configurationsWithPlatforms = ExpandTerms(projectReferenceFacade.AvailableConfigurations, projectReferenceFacade.AvailablePlatforms).ToList(); |
| 61 | |
| 62 | if (configurationsWithPlatforms.Count == 0) |
| 63 | { |
| 64 | yield return projectReferenceFacade; |
| 65 | } |
| 66 | else |
| 67 | { |
| 68 | var expand = new List<ProjectReferenceFacade>(configurationsWithPlatforms.Count) |
| 69 | { |
| 70 | projectReferenceFacade |
| 71 | }; |
| 72 | |
| 73 | // First, clone the project reference so there are enough facades for all of the |
| 74 | // requested configurations/platforms. |
| 75 | for (var i = 1; i < configurationsWithPlatforms.Count; ++i) |
| 76 | { |
| 77 | expand.Add(projectReferenceFacade.Clone()); |
| 78 | } |
| 79 | |
| 80 | // Then set the configuration/platform on each project reference. |
| 81 | for (var i = 0; i < configurationsWithPlatforms.Count; ++i) |
| 82 | { |
| 83 | expand[i].Configuration = configurationsWithPlatforms[i].FirstTerm; |
| 84 | expand[i].Platform = configurationsWithPlatforms[i].SecondTerm; |
| 85 | |
| 86 | yield return expand[i]; |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | private IEnumerable<ProjectReferenceFacade> ExpandProjectReferencesForTargetFrameworksAndRuntimeIdentifiers(IEnumerable<ProjectReferenceFacade> projectReferenceFacades) |
| 93 | { |
| 94 | foreach (var projectReferenceFacade in projectReferenceFacades) |
| 95 | { |
| 96 | var tfmsWithRids = ExpandTerms(projectReferenceFacade.AvailableTargetFrameworks, projectReferenceFacade.AvailableRuntimeIdentifiers).ToList(); |
| 97 | |
| 98 | if (tfmsWithRids.Count == 0) |
| 99 | { |
| 100 | yield return projectReferenceFacade; |
| 101 | } |
| 102 | else |
| 103 | { |
| 104 | var expand = new List<ProjectReferenceFacade>(tfmsWithRids.Count) |
| 105 | { |
| 106 | projectReferenceFacade |
| 107 | }; |
| 108 | |
| 109 | // First, clone the project reference so there are enough facades for all of the |
| 110 | // requested target frameworks/runtime identifiers. |
| 111 | for (var i = 1; i < tfmsWithRids.Count; ++i) |
| 112 | { |
| 113 | expand.Add(projectReferenceFacade.Clone()); |
| 114 | } |
| 115 | |
| 116 | // Then set the target framework/runtime identifier on each project reference. |
| 117 | for (var i = 0; i < tfmsWithRids.Count; ++i) |
| 118 | { |
| 119 | expand[i].TargetFramework = tfmsWithRids[i].FirstTerm; |
| 120 | expand[i].RuntimeIdentifier = tfmsWithRids[i].SecondTerm; |
| 121 | |
| 122 | yield return expand[i]; |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | private IEnumerable<ITaskItem> AssignMetadataToProjectReferences(IEnumerable<ProjectReferenceFacade> facades) |
| 129 | { |
| 130 | foreach (var facade in facades) |
| 131 | { |
| 132 | var projectReference = facade.ProjectReference; |
| 133 | var targetsValue = new MetadataValueList(projectReference, "Targets"); |
| 134 | |
| 135 | if (facade.Modified) |
| 136 | { |
| 137 | var configurationValue = new MetadataValue(projectReference, "Configuration"); |
| 138 | var platformValue = new MetadataValue(projectReference, "Platform"); |
| 139 | var fullConfigurationValue = new MetadataValue(projectReference, "FullConfiguration"); |
| 140 | var additionalProperties = new MetadataValueList(projectReference, "AdditionalProperties"); |
| 141 | var bindName = new MetadataValue(projectReference, "BindName"); |
| 142 | var bindPath = new MetadataValue(projectReference, "BindPath"); |
| 143 | |
| 144 | var publishDir = facade.CalculatePublishDir(); |
| 145 | |
| 146 | additionalProperties.SetValue("PublishDir=", publishDir); |
| 147 | additionalProperties.SetValue("RuntimeIdentifier=", facade.RuntimeIdentifier); |
| 148 | |
| 149 | additionalProperties.Apply(); |
| 150 | |
| 151 | if (!String.IsNullOrWhiteSpace(facade.Configuration)) |
| 152 | { |
| 153 | projectReference.SetMetadata("SetConfiguration", $"Configuration={facade.Configuration}"); |
| 154 | |
| 155 | if (configurationValue.HadValue) |
| 156 | { |
| 157 | configurationValue.SetValue(facade.Configuration); |
| 158 | configurationValue.Apply(); |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | if (!String.IsNullOrWhiteSpace(facade.Platform)) |
| 163 | { |
| 164 | projectReference.SetMetadata("SetPlatform", $"Platform={facade.Platform}"); |
| 165 | |
| 166 | if (platformValue.HadValue) |
| 167 | { |
| 168 | platformValue.SetValue(facade.Platform); |
| 169 | platformValue.Apply(); |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | if (fullConfigurationValue.HadValue && (configurationValue.Modified || platformValue.Modified)) |
| 174 | { |
| 175 | fullConfigurationValue.SetValue($"{configurationValue.Value}|{platformValue.Value}"); |
| 176 | fullConfigurationValue.Apply(); |
| 177 | } |
| 178 | |
| 179 | if (!String.IsNullOrWhiteSpace(facade.TargetFramework)) |
| 180 | { |
| 181 | projectReference.SetMetadata("SetTargetFramework", $"TargetFramework={facade.TargetFramework}"); |
| 182 | } |
| 183 | |
| 184 | var bindNameSuffix = facade.CalculateBindNameSuffix(); |
| 185 | if (!String.IsNullOrWhiteSpace(bindNameSuffix)) |
| 186 | { |
| 187 | var bindNamePrefix = bindName.HadValue ? bindName.Value : Path.GetFileNameWithoutExtension(projectReference.ItemSpec); |
| 188 | |
| 189 | bindName.SetValue(ToolsCommon.CreateIdentifierFromValue(bindNamePrefix + bindNameSuffix)); |
| 190 | bindName.Apply(); |
| 191 | } |
| 192 | |
| 193 | if (!bindPath.HadValue) |
| 194 | { |
| 195 | bindPath.SetValue(publishDir); |
| 196 | bindPath.Apply(); |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | if (facade.Publish) |
| 201 | { |
| 202 | var publishTargets = new MetadataValueList(projectReference, "PublishTargets"); |
| 203 | |
| 204 | if (publishTargets.HadValue) |
| 205 | { |
| 206 | targetsValue.AddRange(publishTargets.Values); |
| 207 | } |
| 208 | else |
| 209 | { |
| 210 | targetsValue.SetValue(null, "Publish"); |
| 211 | } |
| 212 | |
| 213 | // GetTargetPath target always needs to be last so we can set bind paths to the output location of the project reference. |
| 214 | if (targetsValue.Values.Count == 0 || !"GetTargetPath".Equals(targetsValue.Values[targetsValue.Values.Count - 1], StringComparison.OrdinalIgnoreCase)) |
| 215 | { |
| 216 | targetsValue.Values.Remove("GetTargetPath"); |
| 217 | targetsValue.SetValue(null, "GetTargetPath"); |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | if (targetsValue.Modified) |
| 222 | { |
| 223 | targetsValue.Apply(); |
| 224 | } |
| 225 | |
| 226 | this.Log.LogMessage(MessageImportance.Low, "Adding metadata to project reference {0} Targets {1}, BindPath {2}={3}, AdditionalProperties: {4}", |
| 227 | projectReference.ItemSpec, projectReference.GetMetadata("Targets"), projectReference.GetMetadata("BindName"), projectReference.GetMetadata("BindPath"), projectReference.GetMetadata("AdditionalProperties")); |
| 228 | |
| 229 | yield return projectReference; |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | private static IEnumerable<ExpansionTerms> ExpandTerms(IReadOnlyCollection<string> firstTerms, IReadOnlyCollection<string> secondTerms) |
| 234 | { |
| 235 | if (firstTerms.Count == 0) |
| 236 | { |
| 237 | firstTerms = new[] { String.Empty }; |
| 238 | } |
| 239 | |
| 240 | foreach (var firstTerm in firstTerms) |
| 241 | { |
| 242 | var pairSplit = firstTerm.Split(MetadataPairSplitter, 2); |
| 243 | |
| 244 | // No pair indicator so expand the first term by the second term. |
| 245 | if (pairSplit.Length == 1) |
| 246 | { |
| 247 | if (secondTerms.Count == 0) |
| 248 | { |
| 249 | yield return new ExpansionTerms(firstTerm, null); |
| 250 | } |
| 251 | else |
| 252 | { |
| 253 | foreach (var secondTerm in secondTerms) |
| 254 | { |
| 255 | yield return new ExpansionTerms(firstTerm, secondTerm); |
| 256 | } |
| 257 | } |
| 258 | } |
| 259 | else // there was a pair like "first|second" or "first|" or "|second" in the first term, so return that value as the pair. |
| 260 | { |
| 261 | yield return new ExpansionTerms(pairSplit[0], pairSplit[1]); |
| 262 | } |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | private class ProjectReferenceFacade |
| 267 | { |
| 268 | private string configuration; |
| 269 | private string platform; |
| 270 | private string targetFramework; |
| 271 | private string runtimeIdentifier; |
| 272 | |
| 273 | public ProjectReferenceFacade(ITaskItem projectReference, IReadOnlyCollection<string> availableConfigurations, string configuration, IReadOnlyCollection<string> availablePlatforms, string platform, IReadOnlyCollection<string> availableTargetFrameworks, string targetFramework, IReadOnlyCollection<string> availableRuntimeIdentifiers, string runtimeIdentifier, string publishBaseDir) |
| 274 | { |
| 275 | this.ProjectReference = projectReference; |
| 276 | this.AvailableConfigurations = availableConfigurations; |
| 277 | this.configuration = configuration; |
| 278 | this.AvailablePlatforms = availablePlatforms; |
| 279 | this.platform = platform; |
| 280 | this.AvailableTargetFrameworks = availableTargetFrameworks; |
| 281 | this.targetFramework = targetFramework; |
| 282 | this.AvailableRuntimeIdentifiers = availableRuntimeIdentifiers; |
| 283 | this.runtimeIdentifier = runtimeIdentifier; |
| 284 | this.PublishBaseDir = publishBaseDir; |
| 285 | this.Modified = !String.IsNullOrWhiteSpace(configuration) || !String.IsNullOrWhiteSpace(platform) || |
| 286 | !String.IsNullOrWhiteSpace(targetFramework) || !String.IsNullOrWhiteSpace(runtimeIdentifier) || |
| 287 | !String.IsNullOrWhiteSpace(publishBaseDir); |
| 288 | } |
| 289 | |
| 290 | public ITaskItem ProjectReference { get; } |
| 291 | |
| 292 | public bool Modified { get; private set; } |
| 293 | |
| 294 | public IReadOnlyCollection<string> AvailableConfigurations { get; } |
| 295 | |
| 296 | public IReadOnlyCollection<string> AvailablePlatforms { get; } |
| 297 | |
| 298 | public IReadOnlyCollection<string> AvailableRuntimeIdentifiers { get; } |
| 299 | |
| 300 | public IReadOnlyCollection<string> AvailableTargetFrameworks { get; } |
| 301 | |
| 302 | public bool Publish => !String.IsNullOrEmpty(this.PublishBaseDir); |
| 303 | |
| 304 | public string PublishBaseDir { get; } |
| 305 | |
| 306 | public string Configuration |
| 307 | { |
| 308 | get => this.configuration; |
| 309 | set => this.configuration = this.SetWithModified(value, this.configuration); |
| 310 | } |
| 311 | |
| 312 | public string Platform |
| 313 | { |
| 314 | get => this.platform; |
| 315 | set => this.platform = this.SetWithModified(value, this.platform); |
| 316 | } |
| 317 | |
| 318 | public string TargetFramework |
| 319 | { |
| 320 | get => this.targetFramework; |
| 321 | set => this.targetFramework = this.SetWithModified(value, this.targetFramework); |
| 322 | } |
| 323 | |
| 324 | public string RuntimeIdentifier |
| 325 | { |
| 326 | get => this.runtimeIdentifier; |
| 327 | set => this.runtimeIdentifier = this.SetWithModified(value, this.runtimeIdentifier); |
| 328 | } |
| 329 | |
| 330 | public ProjectReferenceFacade Clone() |
| 331 | { |
| 332 | return new ProjectReferenceFacade(new TaskItem(this.ProjectReference), this.AvailableConfigurations, this.configuration, this.AvailablePlatforms, this.platform, this.AvailableTargetFrameworks, this.targetFramework, this.AvailableRuntimeIdentifiers, this.runtimeIdentifier, this.PublishBaseDir); |
| 333 | } |
| 334 | |
| 335 | public static ProjectReferenceFacade CreateFacade(ITaskItem projectReference, TaskLoggingHelper logger, string intermediateFolder) |
| 336 | { |
| 337 | var configurationsValue = new MetadataValueList(projectReference, "Configurations"); |
| 338 | var setConfigurationValue = new MetadataValue(projectReference, "SetConfiguration", "Configuration="); |
| 339 | var platformsValue = new MetadataValueList(projectReference, "Platforms"); |
| 340 | var setPlatformValue = new MetadataValue(projectReference, "SetPlatform", "Platform="); |
| 341 | var targetFrameworksValue = new MetadataValueList(projectReference, "TargetFrameworks"); |
| 342 | var setTargetFrameworkValue = new MetadataValue(projectReference, "SetTargetFramework", "TargetFramework="); |
| 343 | var runtimeIdentifiersValue = new MetadataValueList(projectReference, "RuntimeIdentifiers"); |
| 344 | var publishValue = new MetadataValue(projectReference, "Publish", null); |
| 345 | var publishDirValue = new MetadataValue(projectReference, "PublishDir", null); |
| 346 | |
| 347 | var configurations = GetFromListAndValidateSetValue(configurationsValue, setConfigurationValue, logger, projectReference, "Configuration=Release"); |
| 348 | |
| 349 | var platforms = GetFromListAndValidateSetValue(platformsValue, setPlatformValue, logger, projectReference, "Platform=x64"); |
| 350 | |
| 351 | var targetFrameworks = GetFromListAndValidateSetValue(targetFrameworksValue, setTargetFrameworkValue, logger, projectReference, "TargetFramework=tfm"); |
| 352 | |
| 353 | string publishBaseDir = null; |
| 354 | |
| 355 | if (publishValue.Value.Equals("true", StringComparison.OrdinalIgnoreCase) || (!publishValue.HadValue && publishDirValue.HadValue)) |
| 356 | { |
| 357 | if (publishDirValue.HadValue) |
| 358 | { |
| 359 | publishBaseDir = publishDirValue.Value; |
| 360 | } |
| 361 | else |
| 362 | { |
| 363 | publishBaseDir = Path.Combine(intermediateFolder, "publish", Path.GetFileNameWithoutExtension(projectReference.ItemSpec)); |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | return new ProjectReferenceFacade(projectReference, configurations, null, platforms, null, targetFrameworks, null, runtimeIdentifiersValue.Values, null, publishBaseDir); |
| 368 | } |
| 369 | |
| 370 | public string CalculatePublishDir() |
| 371 | { |
| 372 | if (!this.Publish) |
| 373 | { |
| 374 | return null; |
| 375 | } |
| 376 | |
| 377 | var publishDir = this.PublishBaseDir; |
| 378 | |
| 379 | if (!String.IsNullOrWhiteSpace(this.Configuration)) |
| 380 | { |
| 381 | publishDir = Path.Combine(publishDir, this.Configuration); |
| 382 | } |
| 383 | |
| 384 | if (!String.IsNullOrWhiteSpace(this.Platform)) |
| 385 | { |
| 386 | publishDir = Path.Combine(publishDir, this.Platform); |
| 387 | } |
| 388 | |
| 389 | if (!String.IsNullOrWhiteSpace(this.TargetFramework)) |
| 390 | { |
| 391 | publishDir = Path.Combine(publishDir, this.TargetFramework); |
| 392 | } |
| 393 | |
| 394 | if (!String.IsNullOrWhiteSpace(this.RuntimeIdentifier)) |
| 395 | { |
| 396 | publishDir = Path.Combine(publishDir, this.RuntimeIdentifier); |
| 397 | } |
| 398 | |
| 399 | |
| 400 | return Path.GetFullPath(publishDir); |
| 401 | } |
| 402 | |
| 403 | public string CalculateBindNameSuffix() |
| 404 | { |
| 405 | var sb = new StringBuilder(); |
| 406 | |
| 407 | if (!String.IsNullOrWhiteSpace(this.Configuration)) |
| 408 | { |
| 409 | sb.AppendFormat(".{0}", this.Configuration); |
| 410 | } |
| 411 | |
| 412 | if (!String.IsNullOrWhiteSpace(this.Platform)) |
| 413 | { |
| 414 | sb.AppendFormat(".{0}", this.Platform); |
| 415 | } |
| 416 | |
| 417 | if (!String.IsNullOrWhiteSpace(this.TargetFramework)) |
| 418 | { |
| 419 | sb.AppendFormat(".{0}", this.TargetFramework); |
| 420 | } |
| 421 | |
| 422 | if (!String.IsNullOrWhiteSpace(this.RuntimeIdentifier)) |
| 423 | { |
| 424 | sb.AppendFormat(".{0}", this.RuntimeIdentifier); |
| 425 | } |
| 426 | |
| 427 | return sb.ToString(); |
| 428 | } |
| 429 | |
| 430 | private string SetWithModified(string newValue, string oldValue) |
| 431 | { |
| 432 | if (String.IsNullOrWhiteSpace(newValue) && String.IsNullOrWhiteSpace(oldValue)) |
| 433 | { |
| 434 | return String.Empty; |
| 435 | } |
| 436 | else if (oldValue != newValue) |
| 437 | { |
| 438 | this.Modified = true; |
| 439 | return newValue; |
| 440 | } |
| 441 | |
| 442 | return oldValue; |
| 443 | } |
| 444 | |
| 445 | private static List<string> GetFromListAndValidateSetValue(MetadataValueList listValue, MetadataValue setValue, TaskLoggingHelper logger, ITaskItem projectReference, string setExample) |
| 446 | { |
| 447 | var targetFrameworks = listValue.Values; |
| 448 | |
| 449 | if (setValue.HadValue) |
| 450 | { |
| 451 | if (listValue.HadValue) |
| 452 | { |
| 453 | logger.LogMessage("ProjectReference {0} contains metadata for both {1} and {2}. {2} takes precedent so the {1} value '{3}' will be ignored", projectReference.ItemSpec, setValue.Name, listValue.Name, setValue.OriginalValue); |
| 454 | } |
| 455 | else if (!setValue.ValidValue) |
| 456 | { |
| 457 | logger.LogError("ProjectReference {0} contains invalid {1} value '{2}'. The {1} value should look something like '{3}'.", projectReference.ItemSpec, setValue.Name, setValue.OriginalValue, setExample); |
| 458 | } |
| 459 | } |
| 460 | |
| 461 | return targetFrameworks; |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | private class ExpansionTerms |
| 466 | { |
| 467 | public ExpansionTerms(string firstTerm, string secondTerm) |
| 468 | { |
| 469 | this.FirstTerm = firstTerm; |
| 470 | this.SecondTerm = secondTerm; |
| 471 | } |
| 472 | |
| 473 | public string FirstTerm { get; } |
| 474 | |
| 475 | public string SecondTerm { get; } |
| 476 | } |
| 477 | } |
| 478 | } |