main
cs 478 lines 20.9 KB
Raw
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 }