-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
TaskRegistry.cs
1873 lines (1649 loc) · 94.4 KB
/
TaskRegistry.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 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.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.NET.StringTools;
using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService;
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities;
using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext;
using TaskEngineAssemblyResolver = Microsoft.Build.BackEnd.Logging.TaskEngineAssemblyResolver;
#nullable disable
namespace Microsoft.Build.Execution
{
/// <summary>
/// This class is used to track tasks used by a project. Tasks are declared in project files with the <UsingTask> tag.
/// Task and assembly names must be specified per .NET guidelines, however, the names do not need to be fully qualified if
/// they provide enough information to locate the tasks they refer to. Assemblies can also be referred to using file paths --
/// this is useful when it is not possible/desirable to place task assemblies in the GAC, or in the same directory as MSBuild.
/// </summary>
/// <remarks>
/// 1) specifying a task assembly using BOTH its assembly name (strong or weak) AND its file path is not allowed
/// 2) when specifying the assembly name, the file extension (usually ".dll") must NOT be specified
/// 3) when specifying the assembly file, the file extension MUST be specified
/// </remarks>
/// <example>
/// <UsingTask TaskName="Microsoft.Build.Tasks.Csc" ==> look for the "Csc" task in the
/// AssemblyName="Microsoft.Build.Tasks"/> weakly-named "Microsoft.Build.Tasks" assembly
///
/// <UsingTask TaskName="t1" ==> look for the "t1" task in the
/// AssemblyName="mytasks, Culture=en, Version=1.0.0.0"/> strongly-named "mytasks" assembly
///
/// <UsingTask TaskName="foo" ==> look for the "foo" task in the
/// AssemblyFile="$(MyDownloadedTasks)\utiltasks.dll"/> "utiltasks" assembly file
///
/// <UsingTask TaskName="UtilTasks.Bar" ==> invalid task declaration
/// AssemblyName="utiltasks.dll"
/// AssemblyFile="$(MyDownloadedTasks)\"/>
/// </example>
internal sealed class TaskRegistry : ITranslatable
{
/// <summary>
/// The fallback task registry
/// </summary>
private Toolset _toolset;
/// <summary>
/// If true, we will force all tasks to run in the MSBuild task host EXCEPT
/// a small well-known set of tasks that are known to depend on IBuildEngine
/// callbacks; as forcing those out of proc would be just setting them up for
/// known failure.
/// </summary>
private static bool s_forceTaskHostLaunch = (Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1");
/// <summary>
/// Simple name for the MSBuild tasks (v4), used for shimming in loading
/// task factory UsingTasks
/// </summary>
private static string s_tasksV4SimpleName = "Microsoft.Build.Tasks.v4.0";
/// <summary>
/// Filename for the MSBuild tasks (v4), used for shimming in loading
/// task factory UsingTasks
/// </summary>
private static string s_tasksV4Filename = s_tasksV4SimpleName + ".dll";
/// <summary>
/// Expected location that MSBuild tasks (v4) is picked up from if the user
/// references it with just a simple name, used for shimming in loading
/// task factory UsingTasks
/// </summary>
private static string s_potentialTasksV4Location = Path.Combine(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, s_tasksV4Filename);
/// <summary>
/// Simple name for the MSBuild tasks (v12), used for shimming in loading
/// task factory UsingTasks
/// </summary>
private static string s_tasksV12SimpleName = "Microsoft.Build.Tasks.v12.0";
/// <summary>
/// Filename for the MSBuild tasks (v12), used for shimming in loading
/// task factory UsingTasks
/// </summary>
private static string s_tasksV12Filename = s_tasksV12SimpleName + ".dll";
/// <summary>
/// Expected location that MSBuild tasks (v12) is picked up from if the user
/// references it with just a simple name, used for shimming in loading
/// task factory UsingTasks
/// </summary>
private static string s_potentialTasksV12Location = Path.Combine(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, s_tasksV12Filename);
/// <summary>
/// Simple name for the MSBuild tasks (v14+), used for shimming in loading
/// task factory UsingTasks
/// </summary>
private static string s_tasksCoreSimpleName = "Microsoft.Build.Tasks.Core";
/// <summary>
/// Filename for the MSBuild tasks (v14+), used for shimming in loading
/// task factory UsingTasks
/// </summary>
private static string s_tasksCoreFilename = s_tasksCoreSimpleName + ".dll";
/// <summary>
/// Expected location that MSBuild tasks (v14+) is picked up from if the user
/// references it with just a simple name, used for shimming in loading
/// task factory UsingTasks
/// </summary>
private static string s_potentialTasksCoreLocation = Path.Combine(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, s_tasksCoreFilename);
/// <summary>
/// Monotonically increasing counter for registered tasks.
/// </summary>
private int _nextRegistrationOrderId = 0;
/// <summary>
/// Cache of tasks already found using exact matching,
/// keyed by the task identity requested.
/// </summary>
private readonly ConcurrentDictionary<RegisteredTaskIdentity, RegisteredTaskRecord> _cachedTaskRecordsWithExactMatch =
new(RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact);
/// <summary>
/// Cache of tasks already found using fuzzy matching,
/// keyed by the task name requested.
/// Value is a dictionary of all possible matches for that
/// task name, by unique identity.
/// </summary>
private readonly ConcurrentDictionary<string, ConcurrentDictionary<RegisteredTaskIdentity, RegisteredTaskRecord>> _cachedTaskRecordsWithFuzzyMatch = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Cache of task declarations i.e. the <UsingTask> tags fed to this registry,
/// keyed by the task name declared.
/// Task name may be qualified or not.
/// This field may be null.
/// This is expected to be modified only during initialization via a single call, and all reads will occur only after the initialization is done - so no need for a concurrent dictionary.
/// </summary>
private Dictionary<RegisteredTaskIdentity, List<RegisteredTaskRecord>> _taskRegistrations;
/// <summary>
/// Create another set containing architecture-specific task entries.
/// Then when we look for them, check if the name exists in that.
/// This is expected to be modified only during initialization via a single call, and all reads will occur only after the initialization is done - so no need for a concurrent dictionary.
/// </summary>
private readonly Dictionary<string, List<RegisteredTaskRecord>> _overriddenTasks = new Dictionary<string, List<RegisteredTaskRecord>>();
#if DEBUG
/// <summary>
/// Indicates whether the task registry has been initialized.
/// Task registry cannot be used until it is initialized. And it cannot be initialized more than once.
/// This will help to guarantee logical immutability of TaskRegistry.
/// </summary>
private bool _isInitialized;
#endif
/// <summary>
/// The cache to load the *.tasks files into
/// </summary>
internal ProjectRootElementCacheBase RootElementCache { get; set; }
/// <summary>
/// Creates a task registry that does not fall back to any other task registry.
/// Default constructor does no work because the tables are initialized lazily when a task is registered
/// </summary>
internal TaskRegistry(ProjectRootElementCacheBase projectRootElementCache)
{
ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache, nameof(projectRootElementCache));
RootElementCache = projectRootElementCache;
}
private TaskRegistry()
{
}
/// <summary>
/// Creates a task registry that defers to the specified toolset's registry for those tasks it cannot resolve.
/// UNDONE: (Logging.) We can't pass the base task registry from the Toolset because we can't call GetTaskRegistry
/// without logging context information. When the Project load code is altered to contain logging service
/// references, we can load the toolset task registry at the time this registry is created and pass it to
/// this constructor instead of the toolset state.
/// </summary>
/// <param name="toolset">The Toolset containing the toolser task registry</param>
/// <param name="projectRootElementCache">The <see cref="ProjectRootElementCache"/> to use.</param>
internal TaskRegistry(Toolset toolset, ProjectRootElementCacheBase projectRootElementCache)
{
ErrorUtilities.VerifyThrowInternalNull(projectRootElementCache, nameof(projectRootElementCache));
ErrorUtilities.VerifyThrowInternalNull(toolset, nameof(toolset));
RootElementCache = projectRootElementCache;
_toolset = toolset;
}
/// <summary>
/// Returns the toolset state used to initialize this registry, if any.
/// </summary>
internal Toolset Toolset
{
[DebuggerStepThrough]
get
{ return _toolset; }
}
/// <summary>
/// Access the next registration sequence id.
/// FOR UNIT TESTING ONLY.
/// </summary>
internal int NextRegistrationOrderId => _nextRegistrationOrderId;
/// <summary>
/// Access list of task registrations.
/// FOR UNIT TESTING ONLY.
/// </summary>
internal IDictionary<RegisteredTaskIdentity, List<RegisteredTaskRecord>> TaskRegistrations
{
get
{
if (_taskRegistrations == null)
{
_taskRegistrations = CreateRegisteredTaskDictionary();
}
return _taskRegistrations;
}
}
internal bool IsLoaded => RootElementCache != null;
/// <summary>
/// Evaluate the usingtask and add the result into the data passed in
/// </summary>
/// <typeparam name="P">A type derived from IProperty</typeparam>
/// <typeparam name="I">A type derived from IItem</typeparam>
internal static void InitializeTaskRegistryFromUsingTaskElements<P, I>(
LoggingContext loggingContext,
IEnumerable<(ProjectUsingTaskElement projectUsingTaskXml, string directoryOfImportingFile)> registrations,
TaskRegistry taskRegistry,
Expander<P, I> expander,
ExpanderOptions expanderOptions,
IFileSystem fileSystem)
where P : class, IProperty
where I : class, IItem
{
foreach ((ProjectUsingTaskElement projectUsingTaskXml, string directoryOfImportingFile) registration in registrations)
{
RegisterTasksFromUsingTaskElement(
loggingContext,
registration.directoryOfImportingFile,
registration.projectUsingTaskXml,
taskRegistry,
expander,
expanderOptions,
fileSystem);
}
#if DEBUG
taskRegistry._isInitialized = true;
taskRegistry._taskRegistrations ??= TaskRegistry.CreateRegisteredTaskDictionary();
#endif
}
/// <summary>
/// Evaluate the usingtask and add the result into the data passed in
/// </summary>
/// <typeparam name="P">A type derived from IProperty</typeparam>
/// <typeparam name="I">A type derived from IItem</typeparam>
private static void RegisterTasksFromUsingTaskElement
<P, I>(
LoggingContext loggingContext,
string directoryOfImportingFile,
ProjectUsingTaskElement projectUsingTaskXml,
TaskRegistry taskRegistry,
Expander<P, I> expander,
ExpanderOptions expanderOptions,
IFileSystem fileSystem)
where P : class, IProperty
where I : class, IItem
{
ErrorUtilities.VerifyThrowInternalNull(directoryOfImportingFile, nameof(directoryOfImportingFile));
#if DEBUG
ErrorUtilities.VerifyThrowInternalError(!taskRegistry._isInitialized, "Attempt to modify TaskRegistry after it was initialized.");
#endif
if (!ConditionEvaluator.EvaluateCondition(
projectUsingTaskXml.Condition,
ParserOptions.AllowPropertiesAndItemLists,
expander,
expanderOptions,
projectUsingTaskXml.ContainingProject.DirectoryPath,
projectUsingTaskXml.ConditionLocation,
fileSystem,
loggingContext))
{
return;
}
string assemblyFile = null;
string assemblyName = null;
string taskName = expander.ExpandIntoStringLeaveEscaped(projectUsingTaskXml.TaskName, expanderOptions, projectUsingTaskXml.TaskNameLocation);
ProjectErrorUtilities.VerifyThrowInvalidProject(
taskName.Length > 0,
projectUsingTaskXml.TaskNameLocation,
"InvalidEvaluatedAttributeValue",
taskName,
projectUsingTaskXml.TaskName,
XMakeAttributes.name,
XMakeElements.usingTask);
string taskFactory = expander.ExpandIntoStringLeaveEscaped(projectUsingTaskXml.TaskFactory, expanderOptions, projectUsingTaskXml.TaskFactoryLocation);
if (String.IsNullOrEmpty(taskFactory) || taskFactory.Equals(RegisteredTaskRecord.AssemblyTaskFactory, StringComparison.OrdinalIgnoreCase) || taskFactory.Equals(RegisteredTaskRecord.TaskHostFactory, StringComparison.OrdinalIgnoreCase))
{
ProjectXmlUtilities.VerifyThrowProjectNoChildElements(projectUsingTaskXml.XmlElement);
}
if (projectUsingTaskXml.AssemblyFile.Length > 0)
{
assemblyFile = expander.ExpandIntoStringLeaveEscaped(projectUsingTaskXml.AssemblyFile, expanderOptions, projectUsingTaskXml.AssemblyFileLocation);
}
else
{
assemblyName = expander.ExpandIntoStringLeaveEscaped(projectUsingTaskXml.AssemblyName, expanderOptions, projectUsingTaskXml.AssemblyNameLocation);
}
ProjectErrorUtilities.VerifyThrowInvalidProject(
assemblyFile == null || assemblyFile.Length > 0,
projectUsingTaskXml.AssemblyFileLocation,
"InvalidEvaluatedAttributeValue",
assemblyFile,
projectUsingTaskXml.AssemblyFile,
XMakeAttributes.assemblyFile,
XMakeElements.usingTask);
ProjectErrorUtilities.VerifyThrowInvalidProject(
assemblyName == null || assemblyName.Length > 0,
projectUsingTaskXml.AssemblyNameLocation,
"InvalidEvaluatedAttributeValue",
assemblyName,
projectUsingTaskXml.AssemblyName,
XMakeAttributes.assemblyName,
XMakeElements.usingTask);
// Ensure the assembly file/path is relative to the project in which this <UsingTask> node was defined -- we
// don't want paths from imported projects being interpreted relative to the main project file.
try
{
assemblyFile = FileUtilities.FixFilePath(assemblyFile);
if (assemblyFile != null && !Path.IsPathRooted(assemblyFile))
{
assemblyFile = Strings.WeakIntern(Path.Combine(directoryOfImportingFile, assemblyFile));
}
if (String.Equals(taskFactory, RegisteredTaskRecord.CodeTaskFactory, StringComparison.OrdinalIgnoreCase) || String.Equals(taskFactory, RegisteredTaskRecord.XamlTaskFactory, StringComparison.OrdinalIgnoreCase))
{
// SHIM: One common pattern for people using CodeTaskFactory or XamlTaskFactory from M.B.T.v4.0.dll is to
// specify it using $(MSBuildToolsPath) -- which now no longer contains M.B.T.v4.0.dll. This same pattern
// may also occur if someone is using CodeTaskFactory or XamlTaskFactory from M.B.T.v12.0.dll. So if we have a
// situation where the path being used doesn't contain the v4 or v12 tasks but DOES contain the v14+ tasks, just
// secretly substitute it here.
if (
assemblyFile != null &&
(assemblyFile.EndsWith(s_tasksV4Filename, StringComparison.OrdinalIgnoreCase) || assemblyFile.EndsWith(s_tasksV12Filename, StringComparison.OrdinalIgnoreCase)) &&
!FileUtilities.FileExistsNoThrow(assemblyFile, fileSystem))
{
string replacedAssemblyFile = Path.Combine(Path.GetDirectoryName(assemblyFile), s_tasksCoreFilename);
if (FileUtilities.FileExistsNoThrow(replacedAssemblyFile, fileSystem))
{
assemblyFile = replacedAssemblyFile;
}
}
else if (assemblyName != null)
{
// SHIM: Another common pattern for people using CodeTaskFactory or XamlTaskFactory from
// M.B.T.v4.0.dll is to specify it using AssemblyName with a simple name -- which works only if that
// that assembly is in the current directory. Much like with the above case, if we detect that
// situation, secretly substitute it here so that the majority of task factory users aren't broken.
if
(
assemblyName.Equals(s_tasksV4SimpleName, StringComparison.OrdinalIgnoreCase) &&
!FileUtilities.FileExistsNoThrow(s_potentialTasksV4Location, fileSystem) &&
FileUtilities.FileExistsNoThrow(s_potentialTasksCoreLocation, fileSystem))
{
assemblyName = s_tasksCoreSimpleName;
}
else if
(
assemblyName.Equals(s_tasksV12SimpleName, StringComparison.OrdinalIgnoreCase) &&
!FileUtilities.FileExistsNoThrow(s_potentialTasksV12Location, fileSystem) &&
FileUtilities.FileExistsNoThrow(s_potentialTasksCoreLocation, fileSystem))
{
assemblyName = s_tasksCoreSimpleName;
}
}
}
}
catch (ArgumentException ex)
{
// Invalid chars in AssemblyFile path
ProjectErrorUtilities.ThrowInvalidProject(projectUsingTaskXml.Location, "InvalidAttributeValueWithException", assemblyFile, XMakeAttributes.assemblyFile, XMakeElements.usingTask, ex.Message);
}
RegisteredTaskRecord.ParameterGroupAndTaskElementRecord parameterGroupAndTaskElementRecord = null;
if (projectUsingTaskXml.Count > 0)
{
parameterGroupAndTaskElementRecord = new RegisteredTaskRecord.ParameterGroupAndTaskElementRecord();
parameterGroupAndTaskElementRecord.ExpandUsingTask<P, I>(projectUsingTaskXml, expander, expanderOptions);
}
Dictionary<string, string> taskFactoryParameters = null;
string runtime = expander.ExpandIntoStringLeaveEscaped(projectUsingTaskXml.Runtime, expanderOptions, projectUsingTaskXml.RuntimeLocation);
string architecture = expander.ExpandIntoStringLeaveEscaped(projectUsingTaskXml.Architecture, expanderOptions, projectUsingTaskXml.ArchitectureLocation);
string overrideUsingTask = expander.ExpandIntoStringLeaveEscaped(projectUsingTaskXml.Override, expanderOptions, projectUsingTaskXml.OverrideLocation);
if ((runtime != String.Empty) || (architecture != String.Empty))
{
taskFactoryParameters = CreateTaskFactoryParametersDictionary();
taskFactoryParameters.Add(XMakeAttributes.runtime, runtime == String.Empty ? XMakeAttributes.MSBuildRuntimeValues.any : runtime);
taskFactoryParameters.Add(XMakeAttributes.architecture, architecture == String.Empty ? XMakeAttributes.MSBuildArchitectureValues.any : architecture);
}
taskRegistry.RegisterTask(taskName, AssemblyLoadInfo.Create(assemblyName, assemblyFile), taskFactory, taskFactoryParameters, parameterGroupAndTaskElementRecord, loggingContext, projectUsingTaskXml, ConversionUtilities.ValidBooleanTrue(overrideUsingTask));
}
private static Dictionary<string, string> CreateTaskFactoryParametersDictionary(int? initialCount = null)
{
return initialCount == null
? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, string>(initialCount.Value, StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Given a task name, this method retrieves the task class. If the task has been requested before, it will be found in
/// the class cache; otherwise, <UsingTask> declarations will be used to search the appropriate assemblies.
/// </summary>
internal TaskFactoryWrapper GetRegisteredTask(
string taskName,
string taskProjectFile,
IDictionary<string, string> taskIdentityParameters,
bool exactMatchRequired,
TargetLoggingContext targetLoggingContext,
ElementLocation elementLocation)
{
#if DEBUG
ErrorUtilities.VerifyThrowInternalError(_isInitialized, "Attempt to read from TaskRegistry before its initialization was finished.");
#endif
TaskFactoryWrapper taskFactory = null;
// If there are no usingtask tags in the project don't bother caching or looking for tasks locally
RegisteredTaskRecord record = GetTaskRegistrationRecord(taskName, taskProjectFile, taskIdentityParameters, exactMatchRequired, targetLoggingContext, elementLocation, out bool retrievedFromCache);
if (record != null)
{
// if the given task name is longer than the registered task name
// we will use the longer name to help disambiguate between multiple matches
string mostSpecificTaskName = (taskName.Length > record.RegisteredName.Length) ? taskName : record.RegisteredName;
taskFactory = record.GetTaskFactoryFromRegistrationRecord(mostSpecificTaskName, taskProjectFile, taskIdentityParameters, targetLoggingContext, elementLocation);
if (taskFactory != null && !retrievedFromCache)
{
if (record.TaskFactoryAttributeName.Equals(RegisteredTaskRecord.AssemblyTaskFactory) || record.TaskFactoryAttributeName.Equals(RegisteredTaskRecord.TaskHostFactory))
{
targetLoggingContext.LogComment(MessageImportance.Low, "TaskFound", taskName, taskFactory.Name);
}
else
{
targetLoggingContext.LogComment(MessageImportance.Low, "TaskFoundFromFactory", taskName, taskFactory.Name);
}
if (taskFactory.TaskFactoryLoadedType.HasSTAThreadAttribute)
{
targetLoggingContext.LogComment(MessageImportance.Low, "TaskNeedsSTA", taskName);
}
}
}
return taskFactory;
}
/// <summary>
/// Retrieves the task registration record for the specified task.
/// </summary>
/// <param name="taskName">The name of the task to retrieve.</param>
/// <param name="taskProjectFile">The task's project file.</param>
/// <param name="taskIdentityParameters">The set of task identity parameters to be used to identify the
/// correct task record match.</param>
/// <param name="exactMatchRequired">True if an exact name match is required.</param>
/// <param name="targetLoggingContext">The logging context.</param>
/// <param name="elementLocation">The location of the task element in the project file.</param>
/// <param name="retrievedFromCache">True if the record was retrieved from the cache.</param>
/// <returns>The task registration record, or null if none was found.</returns>
internal RegisteredTaskRecord GetTaskRegistrationRecord(
string taskName,
string taskProjectFile,
IDictionary<string, string> taskIdentityParameters,
bool exactMatchRequired,
TargetLoggingContext targetLoggingContext,
ElementLocation elementLocation,
out bool retrievedFromCache)
{
RegisteredTaskRecord taskRecord = null;
retrievedFromCache = false;
RegisteredTaskIdentity taskIdentity = new RegisteredTaskIdentity(taskName, taskIdentityParameters);
// Project-level override tasks are keyed by task name (unqualified).
// Because Foo.Bar and Baz.Bar are both valid, they are stored
// in a dictionary keyed as `Bar` because most tasks are called unqualified
if (_overriddenTasks.TryGetValue(taskName, out List<RegisteredTaskRecord> recs))
{
// When we determine this task was overridden, search all task records
// to find the most correct registration. Search with the fully qualified name (if applicable)
// Behavior is intended to be "first one wins"
foreach (RegisteredTaskRecord rec in recs)
{
if (RegisteredTaskIdentity.RegisteredTaskIdentityComparer.IsPartialMatch(taskIdentity, rec.TaskIdentity))
{
return rec;
}
}
}
// Try the override task registry first
if (_toolset != null)
{
TaskRegistry toolsetRegistry = _toolset.GetOverrideTaskRegistry(targetLoggingContext, RootElementCache);
taskRecord = toolsetRegistry.GetTaskRegistrationRecord(taskName, taskProjectFile, taskIdentityParameters, exactMatchRequired, targetLoggingContext, elementLocation, out retrievedFromCache);
}
// Try the current task registry
if (taskRecord == null && _taskRegistrations?.Count > 0)
{
if (exactMatchRequired)
{
if (_cachedTaskRecordsWithExactMatch.TryGetValue(taskIdentity, out taskRecord))
{
retrievedFromCache = true;
return taskRecord;
}
}
else
{
if (_cachedTaskRecordsWithFuzzyMatch.TryGetValue(taskIdentity.Name, out ConcurrentDictionary<RegisteredTaskIdentity, RegisteredTaskRecord> taskRecords))
{
// if we've looked up this exact one before, just grab it and return
if (taskRecords.TryGetValue(taskIdentity, out taskRecord))
{
retrievedFromCache = true;
return taskRecord;
}
else
{
// otherwise, check the "short list" of everything else included here to see if one of them matches
foreach (RegisteredTaskRecord record in taskRecords.Values)
{
// Just return the first one that actually matches. There may be nulls in here as well, if we've previously attempted to
// find a variation on this task record and failed. In that case, since it wasn't an exact match (otherwise it would have
// been picked up by the check above) just ignore it, the way we ignore task records that don't work with this set of
// parameters.
if (record != null)
{
if (record.CanTaskBeCreatedByFactory(taskName, taskProjectFile, taskIdentityParameters, targetLoggingContext, elementLocation))
{
retrievedFromCache = true;
return record;
}
}
}
}
// otherwise, nothing fit, so act like we never hit the cache at all.
}
}
IEnumerable<RegisteredTaskRecord> registrations = GetRelevantOrderedRegistrations(taskIdentity, exactMatchRequired);
// look for the given task name in the registry; if not found, gather all registered task names that partially
// match the given name
taskRecord = GetMatchingRegistration(taskName, registrations, taskProjectFile, taskIdentityParameters, targetLoggingContext, elementLocation);
}
// If we didn't find the task but we have a fallback registry in the toolset state, try that one.
if (taskRecord == null && _toolset != null)
{
TaskRegistry toolsetRegistry = _toolset.GetTaskRegistry(targetLoggingContext, RootElementCache);
taskRecord = toolsetRegistry.GetTaskRegistrationRecord(taskName, taskProjectFile, taskIdentityParameters, exactMatchRequired, targetLoggingContext, elementLocation, out retrievedFromCache);
}
// Cache the result, even if it is null. We should never again do the work we just did, for this task name.
if (exactMatchRequired)
{
_cachedTaskRecordsWithExactMatch[taskIdentity] = taskRecord;
}
else
{
// Since this is a fuzzy match, we could conceivably have several sets of task identity parameters that match
// each other ... but might be mutually exclusive themselves. E.g. CLR4|x86 and CLR2|x64 both match *|*.
//
// To prevent us inadvertently leaking something incompatible, in this case, we need to store not just the
// record that we got this time, but ALL of the records that have previously matched this key.
//
// Furthermore, the first level key needs to be the name of the task, not its identity -- otherwise we might
// end up with multiple entries containing subsets of the same fuzzy-matchable tasks. E.g. with the following
// set of steps:
// 1. Look up Foo | bar
// 2. Look up Foo | * (goes into Foo | bar cache entry)
// 3. Look up Foo | baz (gets its own entry because it doesn't match Foo | bar)
// 4. Look up Foo | * (should get the Foo | * under Foo | bar, but depending on what the dictionary looks up
// first, might get Foo | baz, which also matches, instead)
ConcurrentDictionary<RegisteredTaskIdentity, RegisteredTaskRecord> taskRecords
= _cachedTaskRecordsWithFuzzyMatch.GetOrAdd(taskIdentity.Name,
_ => new(RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact));
taskRecords[taskIdentity] = taskRecord;
_cachedTaskRecordsWithFuzzyMatch[taskIdentity.Name] = taskRecords;
}
return taskRecord;
}
/// <summary>
/// Is the class being loaded a task factory class
/// </summary>
private static bool IsTaskFactoryClass(Type type, object unused)
{
return type.GetTypeInfo().IsClass &&
!type.GetTypeInfo().IsAbstract &&
typeof(Microsoft.Build.Framework.ITaskFactory).IsAssignableFrom(type);
}
/// <summary>
/// Searches all task declarations for the given task name.
/// If no exact match is found, looks for partial matches.
/// A task name that is not fully qualified may produce several partial matches.
/// </summary>
private IEnumerable<RegisteredTaskRecord> GetRelevantOrderedRegistrations(RegisteredTaskIdentity taskIdentity, bool exactMatchRequired)
{
if (_taskRegistrations.TryGetValue(taskIdentity, out List<RegisteredTaskRecord> taskAssemblies))
{
// (records for single key should be ordered by order of registrations - as they are inserted into the list)
return taskAssemblies;
}
if (exactMatchRequired)
{
return Enumerable.Empty<RegisteredTaskRecord>();
}
// look through all task declarations for partial matches
return _taskRegistrations
.Where(tp => RegisteredTaskIdentity.RegisteredTaskIdentityComparer.IsPartialMatch(taskIdentity, tp.Key))
.SelectMany(tp => tp.Value)
.OrderBy(r => r.RegistrationOrderId);
}
/// <summary>
/// Registers an evaluated using task tag for future
/// consultation
/// </summary>
private void RegisterTask(
string taskName,
AssemblyLoadInfo assemblyLoadInfo,
string taskFactory,
Dictionary<string, string> taskFactoryParameters,
RegisteredTaskRecord.ParameterGroupAndTaskElementRecord inlineTaskRecord,
LoggingContext loggingContext,
ProjectUsingTaskElement projectUsingTaskInXml,
bool overrideTask = false)
{
ErrorUtilities.VerifyThrowInternalLength(taskName, nameof(taskName));
ErrorUtilities.VerifyThrowInternalNull(assemblyLoadInfo, nameof(assemblyLoadInfo));
// Lazily allocate the hashtable
if (_taskRegistrations == null)
{
_taskRegistrations = CreateRegisteredTaskDictionary();
}
// since more than one task can have the same name, we want to keep track of all assemblies that are declared to
// contain tasks with a given name...
List<RegisteredTaskRecord> registeredTaskEntries;
RegisteredTaskIdentity taskIdentity = new RegisteredTaskIdentity(taskName, taskFactoryParameters);
if (!_taskRegistrations.TryGetValue(taskIdentity, out registeredTaskEntries))
{
registeredTaskEntries = new List<RegisteredTaskRecord>();
_taskRegistrations[taskIdentity] = registeredTaskEntries;
}
RegisteredTaskRecord newRecord = new RegisteredTaskRecord(
taskName,
assemblyLoadInfo,
taskFactory,
taskFactoryParameters,
inlineTaskRecord,
Interlocked.Increment(ref _nextRegistrationOrderId));
if (overrideTask)
{
// Key the dictionary based on Unqualified task names
// This is to support partial matches on tasks like Foo.Bar and Baz.Bar
string[] nameComponents = taskName.Split('.');
string unqualifiedTaskName = nameComponents[nameComponents.Length - 1];
// Is the task already registered?
if (_overriddenTasks.TryGetValue(unqualifiedTaskName, out List<RegisteredTaskRecord> recs))
{
foreach (RegisteredTaskRecord rec in recs)
{
if (rec.RegisteredName.Equals(taskIdentity.Name, StringComparison.OrdinalIgnoreCase))
{
loggingContext.LogError(new BuildEventFileInfo(projectUsingTaskInXml.OverrideLocation), "DuplicateOverrideUsingTaskElement", taskName);
break;
}
}
recs.Add(newRecord);
}
else
{
// New record's name may be fully qualified. Use it anyway to account for partial matches.
List<RegisteredTaskRecord> unqualifiedTaskNameMatches = new();
unqualifiedTaskNameMatches.Add(newRecord);
_overriddenTasks.Add(unqualifiedTaskName, unqualifiedTaskNameMatches);
loggingContext.LogComment(MessageImportance.Low, "OverrideUsingTaskElementCreated", taskName, projectUsingTaskInXml.OverrideLocation);
}
}
registeredTaskEntries.Add(newRecord);
}
private static Dictionary<RegisteredTaskIdentity, List<RegisteredTaskRecord>> CreateRegisteredTaskDictionary(int? capacity = null)
{
return capacity != null
? new Dictionary<RegisteredTaskIdentity, List<RegisteredTaskRecord>>(capacity.Value, RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact)
: new Dictionary<RegisteredTaskIdentity, List<RegisteredTaskRecord>>(RegisteredTaskIdentity.RegisteredTaskIdentityComparer.Exact);
}
/// <summary>
/// Given a task name and a list of records which may contain the task, this helper method will ask the records to see if the task name
/// can be created by the factories which are wrapped by the records. (this is done by instantiating the task factory and asking it).
/// </summary>
private RegisteredTaskRecord GetMatchingRegistration(
string taskName,
IEnumerable<RegisteredTaskRecord> taskRecords,
string taskProjectFile,
IDictionary<string, string> taskIdentityParameters,
TargetLoggingContext targetLoggingContext,
ElementLocation elementLocation)
=>
taskRecords.FirstOrDefault(r =>
r.CanTaskBeCreatedByFactory(
// if the given task name is longer than the registered task name
// we will use the longer name to help disambiguate between multiple matches
(taskName.Length > r.TaskIdentity.Name.Length) ? taskName : r.TaskIdentity.Name,
taskProjectFile,
taskIdentityParameters,
targetLoggingContext,
elementLocation));
/// <summary>
/// An object representing the identity of a task -- not just task name, but also
/// the set of identity parameters
/// </summary>
[DebuggerDisplay("{Name} ParameterCount = {TaskIdentityParameters.Count}")]
internal class RegisteredTaskIdentity : ITranslatable
{
private string _name;
private IDictionary<string, string> _taskIdentityParameters;
/// <summary>
/// Constructor
/// </summary>
internal RegisteredTaskIdentity(string name, IDictionary<string, string> taskIdentityParameters)
{
_name = name;
// The ReadOnlyDictionary is a *wrapper*, the Dictionary is the copy.
_taskIdentityParameters = taskIdentityParameters == null ? null : new ReadOnlyDictionary<string, string>(CreateTaskIdentityParametersDictionary(taskIdentityParameters));
}
private static IDictionary<string, string> CreateTaskIdentityParametersDictionary(IDictionary<string, string> initialState = null, int? initialCount = null)
{
ErrorUtilities.VerifyThrow(initialState == null || initialCount == null, "at most one can be non-null");
if (initialState != null)
{
return new Dictionary<string, string>(initialState, StringComparer.OrdinalIgnoreCase);
}
if (initialCount != null)
{
return new Dictionary<string, string>(initialCount.Value, StringComparer.OrdinalIgnoreCase);
}
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public RegisteredTaskIdentity()
{
}
/// <summary>
/// The name of the task
/// </summary>
public string Name
{
get { return _name; }
}
/// <summary>
/// The identity parameters
/// </summary>
public IDictionary<string, string> TaskIdentityParameters
{
get { return _taskIdentityParameters; }
}
/// <summary>
/// Comparer used to figure out whether two RegisteredTaskIdentities are equal or not.
/// </summary>
internal class RegisteredTaskIdentityComparer : IEqualityComparer<RegisteredTaskIdentity>
{
/// <summary>
/// The singleton comparer to use when an exact match is desired
/// </summary>
private static RegisteredTaskIdentityComparer s_exact = new RegisteredTaskIdentityComparer(true /* exact match */);
/// <summary>
/// The singleton comparer to use when a fuzzy match is desired. Note that this still does an exact match on the
/// name, but does a fuzzy match on the task identity parameters.
/// </summary>
private static RegisteredTaskIdentityComparer s_fuzzy = new RegisteredTaskIdentityComparer(false /* fuzzy match */);
/// <summary>
/// Keeps track of whether we're doing exact or fuzzy equivalency
/// </summary>
private bool _exactMatchRequired;
/// <summary>
/// Constructor
/// </summary>
private RegisteredTaskIdentityComparer(bool exactMatchRequired)
{
_exactMatchRequired = exactMatchRequired;
}
/// <summary>
/// The singleton comparer to use for when an exact match is desired
/// </summary>
public static RegisteredTaskIdentityComparer Exact
{
get { return s_exact; }
}
/// <summary>
/// The singleton comparer to use for when a fuzzy match is desired
/// </summary>
public static RegisteredTaskIdentityComparer Fuzzy
{
get { return s_fuzzy; }
}
/// <summary>
/// Returns true if these two identities match "fuzzily" -- if the names pass a partial type name
/// match and the task identity parameters would constitute a valid merge (e.g. "don't care" and
/// something explicit). Otherwise returns false.
/// </summary>
public static bool IsPartialMatch(RegisteredTaskIdentity x, RegisteredTaskIdentity y)
{
if (TypeLoader.IsPartialTypeNameMatch(x.Name, y.Name))
{
return IdentityParametersMatch(x.TaskIdentityParameters, y.TaskIdentityParameters, false /* fuzzy match */);
}
else
{
return false;
}
}
/// <summary>
/// Returns true if the two task identities are equal; false otherwise.
/// </summary>
public bool Equals(RegisteredTaskIdentity x, RegisteredTaskIdentity y)
{
if (x == null && y == null)
{
return true;
}
if (x == null || y == null)
{
return false;
}
// have to have the same name
if (String.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase))
{
return IdentityParametersMatch(x.TaskIdentityParameters, y.TaskIdentityParameters, _exactMatchRequired);
}
else
{
return false;
}
}
/// <summary>
/// Returns a hash code for the given task identity
/// </summary>
public int GetHashCode(RegisteredTaskIdentity obj)
{
if (obj == null)
{
return 0;
}
int nameHash = String.IsNullOrEmpty(obj.Name) ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name);
// Since equality for the exact comparer depends on the exact values of the parameters,
// we need our hash code to depend on them as well. However, for fuzzy matches, we just
// need the ultimate meaning of the parameters to be the same.
string runtime = null;
string architecture = null;
if (obj.TaskIdentityParameters != null)
{
obj.TaskIdentityParameters.TryGetValue(XMakeAttributes.runtime, out runtime);
obj.TaskIdentityParameters.TryGetValue(XMakeAttributes.architecture, out architecture);
}
int paramHash;
if (_exactMatchRequired)
{
int runtimeHash = runtime == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(runtime);
int architectureHash = architecture == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(architecture);
paramHash = runtimeHash ^ architectureHash;
}
else
{
// Ideally, we'd like a hash code that returns the same thing for any runtime or
// architecture that is counted as a match in Runtime/ArchitectureValuesMatch.
// But since we can't really know that without having someone to compare against,
// in this case just give up and don't try to factor the runtime / architecture
// in, and take the minor hit of having more matching hash codes than we would
// have otherwise.
paramHash = 0;
}
return nameHash ^ paramHash;
}
/// <summary>
/// Returns true if the two dictionaries representing sets of task identity parameters match; false otherwise.
/// Internal so that RegisteredTaskRecord can use this function in its determination of whether the task factory
/// supports a certain task identity.
/// </summary>
private static bool IdentityParametersMatch(IDictionary<string, string> x, IDictionary<string, string> y, bool exactMatchRequired)
{
if (x == null && y == null)
{
return true;
}
if (exactMatchRequired)
{
if (x == null || y == null)
{
return false;
}
if (x.Count != y.Count)
{
return false;
}
// make sure that each parameter value matches as well
foreach (KeyValuePair<string, string> param in x)
{
string value;
if (y.TryGetValue(param.Key, out value))